ue-mcp 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/README.md +4 -3
  2. package/dist/action-class.js +56 -3
  3. package/dist/action-class.js.map +1 -1
  4. package/dist/bridge-timeouts.d.ts +64 -0
  5. package/dist/bridge-timeouts.js +118 -0
  6. package/dist/bridge-timeouts.js.map +1 -0
  7. package/dist/bridge.js +10 -2
  8. package/dist/bridge.js.map +1 -1
  9. package/dist/editor-control.d.ts +72 -5
  10. package/dist/editor-control.js +164 -140
  11. package/dist/editor-control.js.map +1 -1
  12. package/dist/editor-target.d.ts +55 -1
  13. package/dist/editor-target.js +107 -10
  14. package/dist/editor-target.js.map +1 -1
  15. package/dist/engine-observer.d.ts +59 -5
  16. package/dist/engine-observer.js +249 -29
  17. package/dist/engine-observer.js.map +1 -1
  18. package/dist/engine-root.d.ts +91 -0
  19. package/dist/engine-root.js +448 -0
  20. package/dist/engine-root.js.map +1 -0
  21. package/dist/epic-enrich.js +16 -1
  22. package/dist/epic-enrich.js.map +1 -1
  23. package/dist/feedback-cli.js +4 -0
  24. package/dist/feedback-cli.js.map +1 -1
  25. package/dist/feedback-deferred.d.ts +9 -0
  26. package/dist/feedback-deferred.js +2 -0
  27. package/dist/feedback-deferred.js.map +1 -1
  28. package/dist/feedback-fallback.d.ts +99 -0
  29. package/dist/feedback-fallback.js +173 -0
  30. package/dist/feedback-fallback.js.map +1 -0
  31. package/dist/flow/write-methods.js +20 -0
  32. package/dist/flow/write-methods.js.map +1 -1
  33. package/dist/index.js +41 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/lean-context.js +8 -3
  36. package/dist/lean-context.js.map +1 -1
  37. package/dist/locking.js +11 -2
  38. package/dist/locking.js.map +1 -1
  39. package/dist/plugin/loader.d.ts +4 -0
  40. package/dist/plugin/loader.js +22 -8
  41. package/dist/plugin/loader.js.map +1 -1
  42. package/dist/plugin/manifest.d.ts +72 -52
  43. package/dist/plugin/manifest.js +118 -21
  44. package/dist/plugin/manifest.js.map +1 -1
  45. package/dist/plugin-cli.js +16 -3
  46. package/dist/plugin-cli.js.map +1 -1
  47. package/dist/project.d.ts +11 -0
  48. package/dist/project.js +23 -2
  49. package/dist/project.js.map +1 -1
  50. package/dist/python-gate.d.ts +95 -0
  51. package/dist/python-gate.js +175 -0
  52. package/dist/python-gate.js.map +1 -0
  53. package/dist/tool-counts.json +16 -16
  54. package/dist/tools/animation.js +288 -24
  55. package/dist/tools/animation.js.map +1 -1
  56. package/dist/tools/asset.js +79 -14
  57. package/dist/tools/asset.js.map +1 -1
  58. package/dist/tools/audio.js +59 -5
  59. package/dist/tools/audio.js.map +1 -1
  60. package/dist/tools/blueprint.js +29 -12
  61. package/dist/tools/blueprint.js.map +1 -1
  62. package/dist/tools/chooser.js +4 -1
  63. package/dist/tools/chooser.js.map +1 -1
  64. package/dist/tools/editor.js +81 -43
  65. package/dist/tools/editor.js.map +1 -1
  66. package/dist/tools/feedback.js +214 -48
  67. package/dist/tools/feedback.js.map +1 -1
  68. package/dist/tools/foliage.js +27 -1
  69. package/dist/tools/foliage.js.map +1 -1
  70. package/dist/tools/gameplay.js +38 -11
  71. package/dist/tools/gameplay.js.map +1 -1
  72. package/dist/tools/gas.js +17 -5
  73. package/dist/tools/gas.js.map +1 -1
  74. package/dist/tools/landscape.js +22 -7
  75. package/dist/tools/landscape.js.map +1 -1
  76. package/dist/tools/level.js +322 -51
  77. package/dist/tools/level.js.map +1 -1
  78. package/dist/tools/material.js +10 -4
  79. package/dist/tools/material.js.map +1 -1
  80. package/dist/tools/niagara.js +3 -2
  81. package/dist/tools/niagara.js.map +1 -1
  82. package/dist/tools/pcg.js +6 -5
  83. package/dist/tools/pcg.js.map +1 -1
  84. package/dist/tools/plugins.js +2 -0
  85. package/dist/tools/plugins.js.map +1 -1
  86. package/dist/tools/project.js +47 -20
  87. package/dist/tools/project.js.map +1 -1
  88. package/dist/types.d.ts +24 -0
  89. package/dist/types.js +46 -3
  90. package/dist/types.js.map +1 -1
  91. package/dist/ue-mcp.default.yml +493 -125
  92. package/package.json +2 -2
  93. package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
  94. package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
  95. package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
  96. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
  97. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
  98. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
  99. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
  100. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
  101. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
  102. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
  103. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
  104. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
  105. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
  106. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
  107. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
  108. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
  109. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
  110. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
  111. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
  112. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
  113. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
  114. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
  115. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
  116. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
  117. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
  118. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
  119. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
  120. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
  121. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
  122. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
  123. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
  124. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
  125. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
  126. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
  127. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
  128. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
  129. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
  130. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
  131. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
  132. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
  133. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
  134. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
  135. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
  136. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
  137. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
  138. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
  139. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
  140. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
  141. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
  142. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
  143. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
  144. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
  145. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
  146. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
  147. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
  148. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
  149. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
  150. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
  151. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
  152. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
  153. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
  154. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
  155. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
  156. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
  157. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
  158. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
  159. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
  160. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
  161. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
  162. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
  163. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
  164. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
  165. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
  166. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
  167. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
  168. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
  169. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
  170. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
  171. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
  172. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
  173. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
  174. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
  175. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
  176. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
  177. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
  178. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
  179. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
  180. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
  181. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
  182. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
  183. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
  184. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
  185. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
  186. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
  187. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
  188. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
  189. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
  190. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
  191. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
  192. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
  193. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
  194. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
  195. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
  196. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
  197. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
  198. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
  199. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
  200. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
  201. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
  202. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
  203. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
  204. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
  205. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
  206. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
  207. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
  208. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
  209. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
  210. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
  211. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
  212. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
  213. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
  214. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
  215. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
  216. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
  217. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
  218. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
  219. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
  220. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
  221. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
  222. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
  223. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
  224. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
  225. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
  226. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
  227. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
  228. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
  229. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
  230. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
  231. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
  232. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
  233. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
  234. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
  235. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
  236. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
  237. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
  238. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
  239. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
  240. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
  241. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
  242. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
  243. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
  244. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
  245. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
  246. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
  247. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
  248. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
  249. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
  250. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
  251. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
  252. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
  253. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
  254. package/skills/ue-mcp-animation/SKILL.md +154 -0
  255. package/skills/ue-mcp-blueprint/SKILL.md +71 -0
@@ -0,0 +1,902 @@
1
+ // Read-only mesh geometry: per-section vertex data, and mesh measurement.
2
+ //
3
+ // Before this existed the only way to see a vertex was execute_python calling
4
+ // UKismetProceduralMeshLibrary.get_section_from_static_mesh, which works but
5
+ // makes the ProceduralMeshComponent plugin a silent prerequisite for reading a
6
+ // mesh, and returns nothing at all for a skeletal mesh. asset(get_mesh_info)
7
+ // gives a vertex COUNT and asset(get_mesh_bounds) gives a box; neither answers
8
+ // "where on this asset is the grip" (#926), "which triangles carry this UV
9
+ // region" (#948) or "is this mesh watertight and how big is it" (#938).
10
+ //
11
+ // Both handlers read the engine's own render data - FStaticMeshLODResources for
12
+ // a UStaticMesh, FSkeletalMeshLODRenderData for a USkeletalMesh - so there is no
13
+ // plugin dependency and the two asset types answer through one code path.
14
+
15
+ #include "AssetHandlers_Geometry.h"
16
+
17
+ #include "HandlerRegistry.h"
18
+ #include "HandlerUtils.h"
19
+
20
+ #include "Engine/StaticMesh.h"
21
+ #include "Engine/SkeletalMesh.h"
22
+ // FSkeletalMaterial moved out of Engine/SkeletalMesh.h in later UE versions.
23
+ // Pull it explicitly via SkinnedAssetCommon when available.
24
+ #if __has_include("Engine/SkinnedAssetCommon.h")
25
+ #include "Engine/SkinnedAssetCommon.h"
26
+ #endif
27
+ #include "StaticMeshResources.h"
28
+ #include "Materials/MaterialInterface.h"
29
+ #include "Rendering/SkeletalMeshRenderData.h"
30
+ #include "Rendering/SkeletalMeshLODRenderData.h"
31
+
32
+ #include "Dom/JsonObject.h"
33
+ #include "Dom/JsonValue.h"
34
+ #include "HAL/FileManager.h"
35
+ #include "Misc/FileHelper.h"
36
+ #include "Misc/PackageName.h"
37
+ #include "Misc/Paths.h"
38
+ #include "Serialization/JsonSerializer.h"
39
+ #include "Serialization/JsonWriter.h"
40
+
41
+ namespace
42
+ {
43
+ // How many vertices one get_mesh_geometry response may carry inline. A modest
44
+ // prop is a few thousand; a scanned asset is millions. Truncating geometry
45
+ // would be worse than refusing it, because a partial vertex array still indexes
46
+ // as if it were whole, so the cap is a hard stop with dumpToFile as the way
47
+ // past it rather than a silent slice.
48
+ constexpr int32 GeometryInlineVertexCap = 20000;
49
+
50
+ // Above this triangle count the edge-adjacency pass that answers isClosed /
51
+ // isManifold is skipped and reported as skipped. Every other metric is O(n)
52
+ // with no allocation and stays available.
53
+ constexpr int32 GeometryTopologyTriangleCap = 1000000;
54
+
55
+ // Position quantisation used to weld vertices before topology analysis. A UV or
56
+ // smoothing seam splits one physical vertex into several render vertices, so
57
+ // raw index comparison reports a watertight mesh as full of boundary edges.
58
+ constexpr float GeometryWeldTolerance = 0.01f;
59
+
60
+ /** One renderable section of a LOD, described the same way for both mesh types. */
61
+ struct FGeometrySectionInfo
62
+ {
63
+ int32 SectionIndex = 0;
64
+ int32 MaterialIndex = 0;
65
+ FString MaterialSlotName;
66
+ FString MaterialPath;
67
+ uint32 FirstIndex = 0;
68
+ uint32 NumTriangles = 0;
69
+ uint32 MinVertexIndex = 0;
70
+ uint32 MaxVertexIndex = 0;
71
+ bool bDisabled = false;
72
+ };
73
+
74
+ /** Everything read off one LOD, in LOD-global vertex indexing. */
75
+ struct FGeometrySnapshot
76
+ {
77
+ FString AssetType;
78
+ int32 LodCount = 0;
79
+ int32 NumTexCoords = 0;
80
+ TArray<FVector3f> Positions;
81
+ TArray<FVector3f> Normals;
82
+ TArray<FVector2f> UVs; // Only the requested channel.
83
+ TArray<uint32> Indices; // Triangle list, LOD-global vertex indices.
84
+ TArray<FGeometrySectionInfo> Sections;
85
+ FBoxSphereBounds Bounds = FBoxSphereBounds(ForceInit);
86
+ };
87
+
88
+ FString GeometryMaterialPathForSlot(const UMaterialInterface* Material)
89
+ {
90
+ return Material ? Material->GetPathName() : FString();
91
+ }
92
+
93
+ /** Read a static mesh LOD. Returns false with OutError set on any miss. */
94
+ bool BuildGeometrySnapshotFromStaticMesh(
95
+ const UStaticMesh* Mesh,
96
+ int32 LodIndex,
97
+ int32 UVChannel,
98
+ bool bWantNormals,
99
+ bool bWantUVs,
100
+ FGeometrySnapshot& Out,
101
+ FString& OutError)
102
+ {
103
+ const FStaticMeshRenderData* RenderData = Mesh->GetRenderData();
104
+ if (!RenderData || RenderData->LODResources.Num() == 0)
105
+ {
106
+ OutError = FString::Printf(TEXT("StaticMesh '%s' has no render data built"), *Mesh->GetPathName());
107
+ return false;
108
+ }
109
+
110
+ Out.AssetType = TEXT("StaticMesh");
111
+ Out.LodCount = RenderData->LODResources.Num();
112
+ Out.Bounds = Mesh->GetBounds();
113
+
114
+ if (LodIndex < 0 || LodIndex >= Out.LodCount)
115
+ {
116
+ OutError = FString::Printf(TEXT("lodIndex %d out of range (mesh has %d LODs)"), LodIndex, Out.LodCount);
117
+ return false;
118
+ }
119
+
120
+ const FStaticMeshLODResources& LOD = RenderData->LODResources[LodIndex];
121
+ const FPositionVertexBuffer& PositionBuffer = LOD.VertexBuffers.PositionVertexBuffer;
122
+ const FStaticMeshVertexBuffer& VertexBuffer = LOD.VertexBuffers.StaticMeshVertexBuffer;
123
+
124
+ // GetNumVertices is a count that survives the CPU copy being released, so the
125
+ // data pointer is what says whether the vertices are actually readable here.
126
+ // Editor builds keep them; a cooked, streamed build does not.
127
+ const int32 NumVertices = static_cast<int32>(PositionBuffer.GetNumVertices());
128
+ if (NumVertices == 0 || PositionBuffer.GetVertexData() == nullptr)
129
+ {
130
+ OutError = FString::Printf(TEXT("StaticMesh '%s' LOD %d has no CPU-side vertex data"), *Mesh->GetPathName(), LodIndex);
131
+ return false;
132
+ }
133
+
134
+ Out.NumTexCoords = static_cast<int32>(VertexBuffer.GetNumTexCoords());
135
+ if (bWantUVs && Out.NumTexCoords > 0 && (UVChannel < 0 || UVChannel >= Out.NumTexCoords))
136
+ {
137
+ OutError = FString::Printf(TEXT("uvChannel %d out of range (LOD %d has %d UV channels)"), UVChannel, LodIndex, Out.NumTexCoords);
138
+ return false;
139
+ }
140
+
141
+ Out.Positions.Reserve(NumVertices);
142
+ for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex)
143
+ {
144
+ Out.Positions.Add(PositionBuffer.VertexPosition(static_cast<uint32>(VertexIndex)));
145
+ }
146
+
147
+ const int32 NumTangentVertices = static_cast<int32>(VertexBuffer.GetNumVertices());
148
+ const bool bTangentsResident = VertexBuffer.GetTangentData() != nullptr;
149
+ const bool bTexCoordsResident = VertexBuffer.GetTexCoordData() != nullptr;
150
+ if (bWantNormals && bTangentsResident && NumTangentVertices >= NumVertices)
151
+ {
152
+ Out.Normals.Reserve(NumVertices);
153
+ for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex)
154
+ {
155
+ const FVector4f TangentZ = VertexBuffer.VertexTangentZ(static_cast<uint32>(VertexIndex));
156
+ Out.Normals.Add(FVector3f(TangentZ.X, TangentZ.Y, TangentZ.Z));
157
+ }
158
+ }
159
+ if (bWantUVs && Out.NumTexCoords > 0 && bTexCoordsResident && NumTangentVertices >= NumVertices)
160
+ {
161
+ Out.UVs.Reserve(NumVertices);
162
+ for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex)
163
+ {
164
+ Out.UVs.Add(VertexBuffer.GetVertexUV(static_cast<uint32>(VertexIndex), static_cast<uint32>(UVChannel)));
165
+ }
166
+ }
167
+
168
+ // GetCopy memcpys from the stored indices, so ask whether they are stored,
169
+ // not whether the buffer was flagged for CPU access: the editor keeps the
170
+ // data regardless of that flag, and a cooked streamed build drops it.
171
+ if (LOD.IndexBuffer.GetNumIndices() > 0 && LOD.IndexBuffer.GetIndexDataSize() == 0)
172
+ {
173
+ OutError = FString::Printf(
174
+ TEXT("StaticMesh '%s' LOD %d index buffer has no CPU-side copy"),
175
+ *Mesh->GetPathName(), LodIndex);
176
+ return false;
177
+ }
178
+ LOD.IndexBuffer.GetCopy(Out.Indices);
179
+
180
+ const TArray<FStaticMaterial>& Materials = Mesh->GetStaticMaterials();
181
+ for (int32 SectionIndex = 0; SectionIndex < LOD.Sections.Num(); ++SectionIndex)
182
+ {
183
+ const FStaticMeshSection& Section = LOD.Sections[SectionIndex];
184
+ FGeometrySectionInfo Info;
185
+ Info.SectionIndex = SectionIndex;
186
+ Info.MaterialIndex = Section.MaterialIndex;
187
+ Info.FirstIndex = Section.FirstIndex;
188
+ Info.NumTriangles = Section.NumTriangles;
189
+ Info.MinVertexIndex = Section.MinVertexIndex;
190
+ Info.MaxVertexIndex = Section.MaxVertexIndex;
191
+ if (Materials.IsValidIndex(Section.MaterialIndex))
192
+ {
193
+ Info.MaterialSlotName = Materials[Section.MaterialIndex].MaterialSlotName.ToString();
194
+ Info.MaterialPath = GeometryMaterialPathForSlot(Materials[Section.MaterialIndex].MaterialInterface);
195
+ }
196
+ Out.Sections.Add(MoveTemp(Info));
197
+ }
198
+
199
+ return true;
200
+ }
201
+
202
+ /** Read a skeletal mesh LOD. Returns false with OutError set on any miss. */
203
+ bool BuildGeometrySnapshotFromSkeletalMesh(
204
+ const USkeletalMesh* Mesh,
205
+ int32 LodIndex,
206
+ int32 UVChannel,
207
+ bool bWantNormals,
208
+ bool bWantUVs,
209
+ FGeometrySnapshot& Out,
210
+ FString& OutError)
211
+ {
212
+ const FSkeletalMeshRenderData* RenderData = Mesh->GetResourceForRendering();
213
+ if (!RenderData || RenderData->LODRenderData.Num() == 0)
214
+ {
215
+ OutError = FString::Printf(TEXT("SkeletalMesh '%s' has no render data built"), *Mesh->GetPathName());
216
+ return false;
217
+ }
218
+
219
+ Out.AssetType = TEXT("SkeletalMesh");
220
+ Out.LodCount = RenderData->LODRenderData.Num();
221
+ Out.Bounds = Mesh->GetBounds();
222
+
223
+ if (LodIndex < 0 || LodIndex >= Out.LodCount)
224
+ {
225
+ OutError = FString::Printf(TEXT("lodIndex %d out of range (mesh has %d LODs)"), LodIndex, Out.LodCount);
226
+ return false;
227
+ }
228
+
229
+ const FSkeletalMeshLODRenderData& LOD = RenderData->LODRenderData[LodIndex];
230
+ const FPositionVertexBuffer& PositionBuffer = LOD.StaticVertexBuffers.PositionVertexBuffer;
231
+ const FStaticMeshVertexBuffer& VertexBuffer = LOD.StaticVertexBuffers.StaticMeshVertexBuffer;
232
+
233
+ const int32 NumVertices = static_cast<int32>(PositionBuffer.GetNumVertices());
234
+ if (NumVertices == 0 || PositionBuffer.GetVertexData() == nullptr)
235
+ {
236
+ OutError = FString::Printf(TEXT("SkeletalMesh '%s' LOD %d has no CPU-side vertex data"), *Mesh->GetPathName(), LodIndex);
237
+ return false;
238
+ }
239
+
240
+ Out.NumTexCoords = static_cast<int32>(VertexBuffer.GetNumTexCoords());
241
+ if (bWantUVs && Out.NumTexCoords > 0 && (UVChannel < 0 || UVChannel >= Out.NumTexCoords))
242
+ {
243
+ OutError = FString::Printf(TEXT("uvChannel %d out of range (LOD %d has %d UV channels)"), UVChannel, LodIndex, Out.NumTexCoords);
244
+ return false;
245
+ }
246
+
247
+ Out.Positions.Reserve(NumVertices);
248
+ for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex)
249
+ {
250
+ Out.Positions.Add(PositionBuffer.VertexPosition(static_cast<uint32>(VertexIndex)));
251
+ }
252
+
253
+ const int32 NumTangentVertices = static_cast<int32>(VertexBuffer.GetNumVertices());
254
+ const bool bTangentsResident = VertexBuffer.GetTangentData() != nullptr;
255
+ const bool bTexCoordsResident = VertexBuffer.GetTexCoordData() != nullptr;
256
+ if (bWantNormals && bTangentsResident && NumTangentVertices >= NumVertices)
257
+ {
258
+ Out.Normals.Reserve(NumVertices);
259
+ for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex)
260
+ {
261
+ const FVector4f TangentZ = VertexBuffer.VertexTangentZ(static_cast<uint32>(VertexIndex));
262
+ Out.Normals.Add(FVector3f(TangentZ.X, TangentZ.Y, TangentZ.Z));
263
+ }
264
+ }
265
+ if (bWantUVs && Out.NumTexCoords > 0 && bTexCoordsResident && NumTangentVertices >= NumVertices)
266
+ {
267
+ Out.UVs.Reserve(NumVertices);
268
+ for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex)
269
+ {
270
+ Out.UVs.Add(VertexBuffer.GetVertexUV(static_cast<uint32>(VertexIndex), static_cast<uint32>(UVChannel)));
271
+ }
272
+ }
273
+
274
+ if (!LOD.MultiSizeIndexContainer.IsIndexBufferValid())
275
+ {
276
+ OutError = FString::Printf(TEXT("SkeletalMesh '%s' LOD %d has no index buffer"), *Mesh->GetPathName(), LodIndex);
277
+ return false;
278
+ }
279
+ LOD.MultiSizeIndexContainer.GetIndexBuffer(Out.Indices);
280
+
281
+ const TArray<FSkeletalMaterial>& Materials = Mesh->GetMaterials();
282
+ for (int32 SectionIndex = 0; SectionIndex < LOD.RenderSections.Num(); ++SectionIndex)
283
+ {
284
+ const FSkelMeshRenderSection& Section = LOD.RenderSections[SectionIndex];
285
+ FGeometrySectionInfo Info;
286
+ Info.SectionIndex = SectionIndex;
287
+ Info.MaterialIndex = static_cast<int32>(Section.MaterialIndex);
288
+ Info.FirstIndex = Section.BaseIndex;
289
+ Info.NumTriangles = Section.NumTriangles;
290
+ Info.MinVertexIndex = Section.BaseVertexIndex;
291
+ Info.MaxVertexIndex = Section.NumVertices > 0
292
+ ? Section.BaseVertexIndex + Section.NumVertices - 1
293
+ : Section.BaseVertexIndex;
294
+ Info.bDisabled = Section.bDisabled;
295
+ if (Materials.IsValidIndex(Info.MaterialIndex))
296
+ {
297
+ Info.MaterialSlotName = Materials[Info.MaterialIndex].MaterialSlotName.ToString();
298
+ Info.MaterialPath = GeometryMaterialPathForSlot(Materials[Info.MaterialIndex].MaterialInterface);
299
+ }
300
+ Out.Sections.Add(MoveTemp(Info));
301
+ }
302
+
303
+ return true;
304
+ }
305
+
306
+ /** Load the addressed mesh and fill a snapshot, or return the caller's error. */
307
+ TSharedPtr<FJsonValue> BuildGeometrySnapshot(
308
+ const FString& AssetPath,
309
+ int32 LodIndex,
310
+ int32 UVChannel,
311
+ bool bWantNormals,
312
+ bool bWantUVs,
313
+ FGeometrySnapshot& Out)
314
+ {
315
+ FString Error;
316
+
317
+ // Load once as UObject and branch on the concrete type. Loading twice with a
318
+ // typed LoadObject logs a cast failure for whichever type the asset is not,
319
+ // which would put a spurious warning in the editor log on every skeletal
320
+ // mesh read.
321
+ UObject* Asset = LoadAssetByPath<UObject>(AssetPath);
322
+ if (!Asset)
323
+ {
324
+ return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
325
+ }
326
+
327
+ if (const UStaticMesh* StaticMesh = Cast<UStaticMesh>(Asset))
328
+ {
329
+ if (!BuildGeometrySnapshotFromStaticMesh(StaticMesh, LodIndex, UVChannel, bWantNormals, bWantUVs, Out, Error))
330
+ {
331
+ return MCPError(Error);
332
+ }
333
+ return nullptr;
334
+ }
335
+
336
+ if (const USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(Asset))
337
+ {
338
+ if (!BuildGeometrySnapshotFromSkeletalMesh(SkeletalMesh, LodIndex, UVChannel, bWantNormals, bWantUVs, Out, Error))
339
+ {
340
+ return MCPError(Error);
341
+ }
342
+ return nullptr;
343
+ }
344
+
345
+ return MCPError(FString::Printf(
346
+ TEXT("Asset '%s' is a %s, not a StaticMesh or SkeletalMesh"),
347
+ *AssetPath, *Asset->GetClass()->GetName()));
348
+ }
349
+
350
+ /** Section-local triangle span, clamped to what the index buffer actually holds. */
351
+ bool GeometrySectionTriangleRange(
352
+ const FGeometrySnapshot& Snapshot,
353
+ const FGeometrySectionInfo& Section,
354
+ int32& OutFirstIndex,
355
+ int32& OutIndexCount)
356
+ {
357
+ const int64 First = static_cast<int64>(Section.FirstIndex);
358
+ const int64 Count = static_cast<int64>(Section.NumTriangles) * 3;
359
+ if (First < 0 || Count < 0 || First + Count > static_cast<int64>(Snapshot.Indices.Num()))
360
+ {
361
+ return false;
362
+ }
363
+ OutFirstIndex = static_cast<int32>(First);
364
+ OutIndexCount = static_cast<int32>(Count);
365
+ return true;
366
+ }
367
+
368
+ TSharedPtr<FJsonObject> GeometryVec3ToJson(const FVector3f& V)
369
+ {
370
+ return MCPVec3ToJsonObject(FVector(V));
371
+ }
372
+
373
+ TSharedPtr<FJsonObject> GeometryUVToJson(const FVector2f& UV)
374
+ {
375
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
376
+ Obj->SetNumberField(TEXT("u"), UV.X);
377
+ Obj->SetNumberField(TEXT("v"), UV.Y);
378
+ return Obj;
379
+ }
380
+
381
+ FString MakeDefaultGeometryDumpPath(const FString& AssetPath, int32 LodIndex)
382
+ {
383
+ const FString AssetName = FPackageName::GetLongPackageAssetName(AssetPath);
384
+ const FString PathHash = FString::Printf(TEXT("%08x"), GetTypeHash(AssetPath));
385
+ const FString BaseName = FPaths::MakeValidFileName(
386
+ FString::Printf(TEXT("%s_LOD%d_%s"), *AssetName, LodIndex, *PathHash));
387
+ return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("UE_MCP"), TEXT("MeshGeometry"), BaseName + TEXT(".json"));
388
+ }
389
+
390
+ /** Serialise a result object to disk. Mirrors the read_blueprint_graph dump
391
+ * convention: relative paths resolve under Saved/, the directory is created,
392
+ * and the resolved path is echoed back. */
393
+ bool WriteGeometryJsonToFile(
394
+ const TSharedPtr<FJsonObject>& JsonObject,
395
+ const FString& RequestedPath,
396
+ const FString& AssetPath,
397
+ int32 LodIndex,
398
+ FString& OutResolvedPath,
399
+ FString& OutError)
400
+ {
401
+ OutResolvedPath = RequestedPath.IsEmpty() ? MakeDefaultGeometryDumpPath(AssetPath, LodIndex) : RequestedPath;
402
+ if (FPaths::IsRelative(OutResolvedPath))
403
+ {
404
+ OutResolvedPath = FPaths::Combine(FPaths::ProjectSavedDir(), OutResolvedPath);
405
+ }
406
+
407
+ const FString Directory = FPaths::GetPath(OutResolvedPath);
408
+ if (!Directory.IsEmpty() && !IFileManager::Get().MakeDirectory(*Directory, true))
409
+ {
410
+ OutError = FString::Printf(TEXT("Failed to create dump directory: %s"), *Directory);
411
+ return false;
412
+ }
413
+
414
+ FString JsonText;
415
+ const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonText);
416
+ if (!FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer))
417
+ {
418
+ OutError = TEXT("Failed to serialize mesh geometry JSON");
419
+ return false;
420
+ }
421
+
422
+ if (!FFileHelper::SaveStringToFile(JsonText, *OutResolvedPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
423
+ {
424
+ OutError = FString::Printf(TEXT("Failed to write mesh geometry dump: %s"), *OutResolvedPath);
425
+ return false;
426
+ }
427
+
428
+ return true;
429
+ }
430
+
431
+ /** Welded-vertex key so a UV seam does not read as a hole. */
432
+ struct FGeometryWeldKey
433
+ {
434
+ int32 X = 0;
435
+ int32 Y = 0;
436
+ int32 Z = 0;
437
+
438
+ bool operator==(const FGeometryWeldKey& Other) const
439
+ {
440
+ return X == Other.X && Y == Other.Y && Z == Other.Z;
441
+ }
442
+ };
443
+
444
+ uint32 GetTypeHash(const FGeometryWeldKey& Key)
445
+ {
446
+ return HashCombine(HashCombine(::GetTypeHash(Key.X), ::GetTypeHash(Key.Y)), ::GetTypeHash(Key.Z));
447
+ }
448
+
449
+ FGeometryWeldKey MakeGeometryWeldKey(const FVector3f& Position)
450
+ {
451
+ FGeometryWeldKey Key;
452
+ Key.X = FMath::RoundToInt(Position.X / GeometryWeldTolerance);
453
+ Key.Y = FMath::RoundToInt(Position.Y / GeometryWeldTolerance);
454
+ Key.Z = FMath::RoundToInt(Position.Z / GeometryWeldTolerance);
455
+ return Key;
456
+ }
457
+ }
458
+
459
+
460
+ void FAssetGeometryHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
461
+ {
462
+ Registry.RegisterHandler(TEXT("get_mesh_geometry"), &GetMeshGeometry);
463
+ Registry.RegisterHandler(TEXT("measure_mesh_geometry"), &MeasureMeshGeometry);
464
+ }
465
+
466
+
467
+ TSharedPtr<FJsonValue> FAssetGeometryHandlers::GetMeshGeometry(const TSharedPtr<FJsonObject>& Params)
468
+ {
469
+ FString AssetPath;
470
+ if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
471
+
472
+ const int32 LodIndex = OptionalInt(Params, TEXT("lodIndex"), 0);
473
+ const int32 UVChannel = OptionalInt(Params, TEXT("uvChannel"), 0);
474
+ const bool bDumpToFile = OptionalBool(Params, TEXT("dumpToFile"), false);
475
+ const FString OutputPath = OptionalString(Params, TEXT("outputPath"), TEXT(""));
476
+
477
+ const bool bHasSectionIndex = Params->HasField(TEXT("sectionIndex"));
478
+ const int32 RequestedSection = OptionalInt(Params, TEXT("sectionIndex"), 0);
479
+
480
+ // `include` selects which arrays come back. Omitted means all four, which is
481
+ // what a caller who does not know what they need should get.
482
+ bool bWantPositions = true;
483
+ bool bWantUVs = true;
484
+ bool bWantNormals = true;
485
+ bool bWantTriangles = true;
486
+ const TArray<TSharedPtr<FJsonValue>>* IncludeArray = nullptr;
487
+ if (Params->TryGetArrayField(TEXT("include"), IncludeArray) && IncludeArray)
488
+ {
489
+ bWantPositions = bWantUVs = bWantNormals = bWantTriangles = false;
490
+ for (const TSharedPtr<FJsonValue>& Entry : *IncludeArray)
491
+ {
492
+ if (!Entry.IsValid()) continue;
493
+ const FString Token = Entry->AsString().ToLower();
494
+ if (Token == TEXT("positions")) bWantPositions = true;
495
+ else if (Token == TEXT("uvs")) bWantUVs = true;
496
+ else if (Token == TEXT("normals")) bWantNormals = true;
497
+ else if (Token == TEXT("triangles")) bWantTriangles = true;
498
+ else
499
+ {
500
+ return MCPError(FString::Printf(
501
+ TEXT("include entry '%s' is not one of: positions, uvs, normals, triangles"),
502
+ *Entry->AsString()));
503
+ }
504
+ }
505
+ if (!bWantPositions && !bWantUVs && !bWantNormals && !bWantTriangles)
506
+ {
507
+ return MCPError(TEXT("'include' was empty: pass at least one of positions, uvs, normals, triangles"));
508
+ }
509
+ }
510
+
511
+ FGeometrySnapshot Snapshot;
512
+ if (auto Err = BuildGeometrySnapshot(AssetPath, LodIndex, UVChannel, bWantNormals, bWantUVs, Snapshot))
513
+ {
514
+ return Err;
515
+ }
516
+
517
+ TArray<int32> SectionsToEmit;
518
+ if (bHasSectionIndex)
519
+ {
520
+ if (!Snapshot.Sections.IsValidIndex(RequestedSection))
521
+ {
522
+ return MCPError(FString::Printf(
523
+ TEXT("sectionIndex %d out of range (LOD %d has %d sections)"),
524
+ RequestedSection, LodIndex, Snapshot.Sections.Num()));
525
+ }
526
+ SectionsToEmit.Add(RequestedSection);
527
+ }
528
+ else
529
+ {
530
+ for (int32 Index = 0; Index < Snapshot.Sections.Num(); ++Index)
531
+ {
532
+ SectionsToEmit.Add(Index);
533
+ }
534
+ }
535
+
536
+ // Size gate. Count what would actually be emitted, not the whole LOD, so
537
+ // asking for one section of a scanned asset still answers.
538
+ int64 EmittedVertices = 0;
539
+ for (int32 SectionIndex : SectionsToEmit)
540
+ {
541
+ const FGeometrySectionInfo& Section = Snapshot.Sections[SectionIndex];
542
+ EmittedVertices += static_cast<int64>(Section.MaxVertexIndex) - static_cast<int64>(Section.MinVertexIndex) + 1;
543
+ }
544
+ if (!bDumpToFile && EmittedVertices > GeometryInlineVertexCap)
545
+ {
546
+ return MCPError(FString::Printf(
547
+ TEXT("This request would return %lld vertices, over the %d inline limit. Pass dumpToFile=true to write the full data to a file, or narrow it with sectionIndex / a higher lodIndex."),
548
+ EmittedVertices, GeometryInlineVertexCap));
549
+ }
550
+
551
+ TArray<TSharedPtr<FJsonValue>> SectionObjects;
552
+ int64 TotalEmittedTriangles = 0;
553
+ for (int32 SectionIndex : SectionsToEmit)
554
+ {
555
+ const FGeometrySectionInfo& Section = Snapshot.Sections[SectionIndex];
556
+
557
+ TSharedPtr<FJsonObject> SectionObj = MakeShared<FJsonObject>();
558
+ SectionObj->SetNumberField(TEXT("sectionIndex"), Section.SectionIndex);
559
+ SectionObj->SetNumberField(TEXT("materialIndex"), Section.MaterialIndex);
560
+ if (!Section.MaterialSlotName.IsEmpty()) SectionObj->SetStringField(TEXT("materialSlotName"), Section.MaterialSlotName);
561
+ if (!Section.MaterialPath.IsEmpty()) SectionObj->SetStringField(TEXT("materialPath"), Section.MaterialPath);
562
+ SectionObj->SetNumberField(TEXT("firstIndex"), Section.FirstIndex);
563
+ SectionObj->SetNumberField(TEXT("triangleCount"), Section.NumTriangles);
564
+ SectionObj->SetNumberField(TEXT("baseVertexIndex"), Section.MinVertexIndex);
565
+ SectionObj->SetNumberField(TEXT("maxVertexIndex"), Section.MaxVertexIndex);
566
+ if (Section.bDisabled) SectionObj->SetBoolField(TEXT("disabled"), true);
567
+
568
+ const int32 SectionFirstVertex = static_cast<int32>(Section.MinVertexIndex);
569
+ const int32 SectionLastVertex = static_cast<int32>(Section.MaxVertexIndex);
570
+ const int32 SectionVertexCount = SectionLastVertex - SectionFirstVertex + 1;
571
+ SectionObj->SetNumberField(TEXT("vertexCount"), SectionVertexCount);
572
+
573
+ if (bWantPositions)
574
+ {
575
+ TArray<TSharedPtr<FJsonValue>> Positions;
576
+ Positions.Reserve(SectionVertexCount);
577
+ for (int32 VertexIndex = SectionFirstVertex; VertexIndex <= SectionLastVertex; ++VertexIndex)
578
+ {
579
+ if (!Snapshot.Positions.IsValidIndex(VertexIndex)) break;
580
+ Positions.Add(MakeShared<FJsonValueObject>(GeometryVec3ToJson(Snapshot.Positions[VertexIndex])));
581
+ }
582
+ SectionObj->SetArrayField(TEXT("positions"), Positions);
583
+ }
584
+
585
+ if (bWantNormals)
586
+ {
587
+ TArray<TSharedPtr<FJsonValue>> Normals;
588
+ Normals.Reserve(SectionVertexCount);
589
+ for (int32 VertexIndex = SectionFirstVertex; VertexIndex <= SectionLastVertex; ++VertexIndex)
590
+ {
591
+ if (!Snapshot.Normals.IsValidIndex(VertexIndex)) break;
592
+ Normals.Add(MakeShared<FJsonValueObject>(GeometryVec3ToJson(Snapshot.Normals[VertexIndex])));
593
+ }
594
+ SectionObj->SetArrayField(TEXT("normals"), Normals);
595
+ }
596
+
597
+ if (bWantUVs)
598
+ {
599
+ TArray<TSharedPtr<FJsonValue>> UVs;
600
+ UVs.Reserve(SectionVertexCount);
601
+ for (int32 VertexIndex = SectionFirstVertex; VertexIndex <= SectionLastVertex; ++VertexIndex)
602
+ {
603
+ if (!Snapshot.UVs.IsValidIndex(VertexIndex)) break;
604
+ UVs.Add(MakeShared<FJsonValueObject>(GeometryUVToJson(Snapshot.UVs[VertexIndex])));
605
+ }
606
+ SectionObj->SetArrayField(TEXT("uvs"), UVs);
607
+ }
608
+
609
+ if (bWantTriangles)
610
+ {
611
+ int32 FirstIndex = 0;
612
+ int32 IndexCount = 0;
613
+ TArray<TSharedPtr<FJsonValue>> Triangles;
614
+ if (GeometrySectionTriangleRange(Snapshot, Section, FirstIndex, IndexCount))
615
+ {
616
+ Triangles.Reserve(IndexCount);
617
+ for (int32 Offset = 0; Offset < IndexCount; ++Offset)
618
+ {
619
+ // Section-local, so it indexes this section's own positions
620
+ // array directly. baseVertexIndex maps it back to LOD-global.
621
+ const int64 Local = static_cast<int64>(Snapshot.Indices[FirstIndex + Offset]) - SectionFirstVertex;
622
+ Triangles.Add(MakeShared<FJsonValueNumber>(static_cast<double>(Local)));
623
+ }
624
+ }
625
+ SectionObj->SetArrayField(TEXT("triangles"), Triangles);
626
+ SectionObj->SetBoolField(TEXT("trianglesAreSectionLocal"), true);
627
+ }
628
+
629
+ TotalEmittedTriangles += Section.NumTriangles;
630
+ SectionObjects.Add(MakeShared<FJsonValueObject>(SectionObj));
631
+ }
632
+
633
+ auto BuildResult = [&]() -> TSharedPtr<FJsonObject>
634
+ {
635
+ auto Result = MCPSuccess();
636
+ Result->SetStringField(TEXT("assetPath"), AssetPath);
637
+ Result->SetStringField(TEXT("assetType"), Snapshot.AssetType);
638
+ Result->SetNumberField(TEXT("lodIndex"), LodIndex);
639
+ Result->SetNumberField(TEXT("lodCount"), Snapshot.LodCount);
640
+ Result->SetNumberField(TEXT("uvChannel"), UVChannel);
641
+ Result->SetNumberField(TEXT("uvChannelCount"), Snapshot.NumTexCoords);
642
+ Result->SetNumberField(TEXT("vertexCount"), Snapshot.Positions.Num());
643
+ Result->SetNumberField(TEXT("triangleCount"), Snapshot.Indices.Num() / 3);
644
+ Result->SetNumberField(TEXT("sectionCount"), Snapshot.Sections.Num());
645
+ Result->SetNumberField(TEXT("emittedVertexCount"), static_cast<double>(EmittedVertices));
646
+ Result->SetNumberField(TEXT("emittedTriangleCount"), static_cast<double>(TotalEmittedTriangles));
647
+ Result->SetArrayField(TEXT("sections"), SectionObjects);
648
+ return Result;
649
+ };
650
+
651
+ if (bDumpToFile)
652
+ {
653
+ const TSharedPtr<FJsonObject> DumpResult = BuildResult();
654
+ FString ResolvedDumpPath;
655
+ FString DumpError;
656
+ if (!WriteGeometryJsonToFile(DumpResult, OutputPath, AssetPath, LodIndex, ResolvedDumpPath, DumpError))
657
+ {
658
+ return MCPError(DumpError);
659
+ }
660
+
661
+ auto Result = MCPSuccess();
662
+ Result->SetStringField(TEXT("assetPath"), AssetPath);
663
+ Result->SetStringField(TEXT("assetType"), Snapshot.AssetType);
664
+ Result->SetNumberField(TEXT("lodIndex"), LodIndex);
665
+ Result->SetNumberField(TEXT("lodCount"), Snapshot.LodCount);
666
+ Result->SetBoolField(TEXT("dumpedToFile"), true);
667
+ Result->SetStringField(TEXT("outputPath"), ResolvedDumpPath);
668
+ Result->SetNumberField(TEXT("vertexCount"), Snapshot.Positions.Num());
669
+ Result->SetNumberField(TEXT("triangleCount"), Snapshot.Indices.Num() / 3);
670
+ Result->SetNumberField(TEXT("sectionCount"), Snapshot.Sections.Num());
671
+ Result->SetNumberField(TEXT("emittedVertexCount"), static_cast<double>(EmittedVertices));
672
+ Result->SetNumberField(TEXT("emittedTriangleCount"), static_cast<double>(TotalEmittedTriangles));
673
+ return MCPResult(Result);
674
+ }
675
+
676
+ return MCPResult(BuildResult());
677
+ }
678
+
679
+
680
+ TSharedPtr<FJsonValue> FAssetGeometryHandlers::MeasureMeshGeometry(const TSharedPtr<FJsonObject>& Params)
681
+ {
682
+ FString AssetPath;
683
+ if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
684
+
685
+ const int32 LodIndex = OptionalInt(Params, TEXT("lodIndex"), 0);
686
+ const bool bHasSectionIndex = Params->HasField(TEXT("sectionIndex"));
687
+ const int32 RequestedSection = OptionalInt(Params, TEXT("sectionIndex"), 0);
688
+
689
+ FGeometrySnapshot Snapshot;
690
+ if (auto Err = BuildGeometrySnapshot(AssetPath, LodIndex, /*UVChannel=*/0, /*bWantNormals=*/false, /*bWantUVs=*/false, Snapshot))
691
+ {
692
+ return Err;
693
+ }
694
+
695
+ TArray<int32> SectionsToMeasure;
696
+ if (bHasSectionIndex)
697
+ {
698
+ if (!Snapshot.Sections.IsValidIndex(RequestedSection))
699
+ {
700
+ return MCPError(FString::Printf(
701
+ TEXT("sectionIndex %d out of range (LOD %d has %d sections)"),
702
+ RequestedSection, LodIndex, Snapshot.Sections.Num()));
703
+ }
704
+ SectionsToMeasure.Add(RequestedSection);
705
+ }
706
+ else
707
+ {
708
+ for (int32 Index = 0; Index < Snapshot.Sections.Num(); ++Index)
709
+ {
710
+ SectionsToMeasure.Add(Index);
711
+ }
712
+ }
713
+
714
+ // Gather the triangles under measurement once, then every metric reads the
715
+ // same set. Sections can overlap vertex ranges, so triangles, not vertices,
716
+ // are the unit.
717
+ TArray<int32> TriangleStarts;
718
+ for (int32 SectionIndex : SectionsToMeasure)
719
+ {
720
+ int32 FirstIndex = 0;
721
+ int32 IndexCount = 0;
722
+ if (!GeometrySectionTriangleRange(Snapshot, Snapshot.Sections[SectionIndex], FirstIndex, IndexCount))
723
+ {
724
+ continue;
725
+ }
726
+ for (int32 Offset = 0; Offset + 2 < IndexCount; Offset += 3)
727
+ {
728
+ TriangleStarts.Add(FirstIndex + Offset);
729
+ }
730
+ }
731
+
732
+ // #938: get_mesh_volume_area() in the engine returns (SurfaceArea, Volume) in
733
+ // that order, which is the reverse of what its name says, and the two are
734
+ // plausible numbers for each other so a swap is invisible. Both come back
735
+ // here as named fields for exactly that reason: there is no positional pair
736
+ // to get the wrong way round.
737
+ double SurfaceArea = 0.0;
738
+ double SignedVolume = 0.0;
739
+ FBox LocalBox(ForceInit);
740
+ TSet<int32> TouchedVertices;
741
+
742
+ for (int32 TriangleStart : TriangleStarts)
743
+ {
744
+ const uint32 I0 = Snapshot.Indices[TriangleStart];
745
+ const uint32 I1 = Snapshot.Indices[TriangleStart + 1];
746
+ const uint32 I2 = Snapshot.Indices[TriangleStart + 2];
747
+ if (!Snapshot.Positions.IsValidIndex(static_cast<int32>(I0)) ||
748
+ !Snapshot.Positions.IsValidIndex(static_cast<int32>(I1)) ||
749
+ !Snapshot.Positions.IsValidIndex(static_cast<int32>(I2)))
750
+ {
751
+ continue;
752
+ }
753
+
754
+ const FVector V0(Snapshot.Positions[static_cast<int32>(I0)]);
755
+ const FVector V1(Snapshot.Positions[static_cast<int32>(I1)]);
756
+ const FVector V2(Snapshot.Positions[static_cast<int32>(I2)]);
757
+
758
+ SurfaceArea += 0.5 * FVector::CrossProduct(V1 - V0, V2 - V0).Size();
759
+ // Signed tetrahedron volume against the origin. Summed over a closed
760
+ // surface this is the enclosed volume; over an open one it is meaningless,
761
+ // which is why isClosed is reported next to it.
762
+ SignedVolume += FVector::DotProduct(V0, FVector::CrossProduct(V1, V2)) / 6.0;
763
+
764
+ LocalBox += V0;
765
+ LocalBox += V1;
766
+ LocalBox += V2;
767
+ TouchedVertices.Add(static_cast<int32>(I0));
768
+ TouchedVertices.Add(static_cast<int32>(I1));
769
+ TouchedVertices.Add(static_cast<int32>(I2));
770
+ }
771
+
772
+ // Topology: weld by quantised position first, so a UV seam does not read as
773
+ // an open boundary, then count how many triangles share each undirected edge.
774
+ bool bTopologyAnalyzed = false;
775
+ bool bIsClosed = false;
776
+ bool bIsManifold = false;
777
+ int32 BoundaryEdgeCount = 0;
778
+ int32 NonManifoldEdgeCount = 0;
779
+ FString TopologySkippedReason;
780
+
781
+ if (TriangleStarts.Num() > GeometryTopologyTriangleCap)
782
+ {
783
+ TopologySkippedReason = FString::Printf(
784
+ TEXT("%d triangles is over the %d triangle limit for edge-adjacency analysis"),
785
+ TriangleStarts.Num(), GeometryTopologyTriangleCap);
786
+ }
787
+ else if (TriangleStarts.Num() > 0)
788
+ {
789
+ TMap<FGeometryWeldKey, int32> WeldedIds;
790
+ WeldedIds.Reserve(Snapshot.Positions.Num());
791
+ TArray<int32> VertexToWelded;
792
+ VertexToWelded.Init(INDEX_NONE, Snapshot.Positions.Num());
793
+ int32 NextWeldedId = 0;
794
+ for (int32 VertexIndex = 0; VertexIndex < Snapshot.Positions.Num(); ++VertexIndex)
795
+ {
796
+ const FGeometryWeldKey Key = MakeGeometryWeldKey(Snapshot.Positions[VertexIndex]);
797
+ if (const int32* Existing = WeldedIds.Find(Key))
798
+ {
799
+ VertexToWelded[VertexIndex] = *Existing;
800
+ }
801
+ else
802
+ {
803
+ WeldedIds.Add(Key, NextWeldedId);
804
+ VertexToWelded[VertexIndex] = NextWeldedId;
805
+ ++NextWeldedId;
806
+ }
807
+ }
808
+
809
+ TMap<uint64, int32> EdgeUseCount;
810
+ EdgeUseCount.Reserve(TriangleStarts.Num() * 3);
811
+ auto AddEdge = [&EdgeUseCount](int32 A, int32 B)
812
+ {
813
+ const uint32 Low = static_cast<uint32>(FMath::Min(A, B));
814
+ const uint32 High = static_cast<uint32>(FMath::Max(A, B));
815
+ const uint64 Key = (static_cast<uint64>(Low) << 32) | static_cast<uint64>(High);
816
+ EdgeUseCount.FindOrAdd(Key)++;
817
+ };
818
+
819
+ for (int32 TriangleStart : TriangleStarts)
820
+ {
821
+ const int32 I0 = static_cast<int32>(Snapshot.Indices[TriangleStart]);
822
+ const int32 I1 = static_cast<int32>(Snapshot.Indices[TriangleStart + 1]);
823
+ const int32 I2 = static_cast<int32>(Snapshot.Indices[TriangleStart + 2]);
824
+ if (!VertexToWelded.IsValidIndex(I0) || !VertexToWelded.IsValidIndex(I1) || !VertexToWelded.IsValidIndex(I2))
825
+ {
826
+ continue;
827
+ }
828
+ const int32 W0 = VertexToWelded[I0];
829
+ const int32 W1 = VertexToWelded[I1];
830
+ const int32 W2 = VertexToWelded[I2];
831
+ // A degenerate triangle contributes no real edges.
832
+ if (W0 == W1 || W1 == W2 || W0 == W2) continue;
833
+ AddEdge(W0, W1);
834
+ AddEdge(W1, W2);
835
+ AddEdge(W2, W0);
836
+ }
837
+
838
+ for (const TPair<uint64, int32>& Edge : EdgeUseCount)
839
+ {
840
+ if (Edge.Value == 1) ++BoundaryEdgeCount;
841
+ else if (Edge.Value > 2) ++NonManifoldEdgeCount;
842
+ }
843
+
844
+ bTopologyAnalyzed = true;
845
+ bIsManifold = NonManifoldEdgeCount == 0;
846
+ bIsClosed = bIsManifold && BoundaryEdgeCount == 0;
847
+ }
848
+
849
+ auto Result = MCPSuccess();
850
+ Result->SetStringField(TEXT("assetPath"), AssetPath);
851
+ Result->SetStringField(TEXT("assetType"), Snapshot.AssetType);
852
+ Result->SetNumberField(TEXT("lodIndex"), LodIndex);
853
+ Result->SetNumberField(TEXT("lodCount"), Snapshot.LodCount);
854
+ Result->SetNumberField(TEXT("sectionCount"), Snapshot.Sections.Num());
855
+ if (bHasSectionIndex) Result->SetNumberField(TEXT("sectionIndex"), RequestedSection);
856
+
857
+ Result->SetNumberField(TEXT("triangleCount"), TriangleStarts.Num());
858
+ Result->SetNumberField(TEXT("vertexCount"), TouchedVertices.Num());
859
+ Result->SetNumberField(TEXT("lodVertexCount"), Snapshot.Positions.Num());
860
+ Result->SetNumberField(TEXT("lodTriangleCount"), Snapshot.Indices.Num() / 3);
861
+
862
+ Result->SetNumberField(TEXT("surfaceArea"), SurfaceArea);
863
+ Result->SetNumberField(TEXT("volume"), FMath::Abs(SignedVolume));
864
+ Result->SetNumberField(TEXT("signedVolume"), SignedVolume);
865
+ Result->SetBoolField(TEXT("volumeIsMeaningful"), bTopologyAnalyzed && bIsClosed);
866
+ Result->SetStringField(TEXT("units"), TEXT("surfaceArea in cm2, volume in cm3, lengths in cm (Unreal units)"));
867
+
868
+ TSharedPtr<FJsonObject> BoundsObj = MakeShared<FJsonObject>();
869
+ const FVector BoxMin = LocalBox.IsValid ? LocalBox.Min : FVector::ZeroVector;
870
+ const FVector BoxMax = LocalBox.IsValid ? LocalBox.Max : FVector::ZeroVector;
871
+ BoundsObj->SetObjectField(TEXT("min"), MCPVec3ToJsonObject(BoxMin));
872
+ BoundsObj->SetObjectField(TEXT("max"), MCPVec3ToJsonObject(BoxMax));
873
+ BoundsObj->SetObjectField(TEXT("center"), MCPVec3ToJsonObject((BoxMin + BoxMax) * 0.5));
874
+ BoundsObj->SetObjectField(TEXT("extent"), MCPVec3ToJsonObject((BoxMax - BoxMin) * 0.5));
875
+ Result->SetObjectField(TEXT("bounds"), BoundsObj);
876
+ Result->SetObjectField(TEXT("dimensions"), MCPVec3ToJsonObject(BoxMax - BoxMin));
877
+
878
+ // The asset's own cached bounds, which include every LOD and any extension
879
+ // the artist set, kept separate from the measured box above so the two are
880
+ // never confused for one another.
881
+ TSharedPtr<FJsonObject> AssetBoundsObj = MakeShared<FJsonObject>();
882
+ AssetBoundsObj->SetObjectField(TEXT("origin"), MCPVec3ToJsonObject(Snapshot.Bounds.Origin));
883
+ AssetBoundsObj->SetObjectField(TEXT("boxExtent"), MCPVec3ToJsonObject(Snapshot.Bounds.BoxExtent));
884
+ AssetBoundsObj->SetNumberField(TEXT("sphereRadius"), Snapshot.Bounds.SphereRadius);
885
+ Result->SetObjectField(TEXT("assetBounds"), AssetBoundsObj);
886
+
887
+ Result->SetBoolField(TEXT("topologyAnalyzed"), bTopologyAnalyzed);
888
+ if (bTopologyAnalyzed)
889
+ {
890
+ Result->SetBoolField(TEXT("isClosed"), bIsClosed);
891
+ Result->SetBoolField(TEXT("isManifold"), bIsManifold);
892
+ Result->SetNumberField(TEXT("boundaryEdgeCount"), BoundaryEdgeCount);
893
+ Result->SetNumberField(TEXT("nonManifoldEdgeCount"), NonManifoldEdgeCount);
894
+ Result->SetNumberField(TEXT("weldToleranceCm"), GeometryWeldTolerance);
895
+ }
896
+ else if (!TopologySkippedReason.IsEmpty())
897
+ {
898
+ Result->SetStringField(TEXT("topologySkippedReason"), TopologySkippedReason);
899
+ }
900
+
901
+ return MCPResult(Result);
902
+ }