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.
- package/README.md +4 -3
- package/dist/action-class.js +56 -3
- package/dist/action-class.js.map +1 -1
- package/dist/bridge-timeouts.d.ts +64 -0
- package/dist/bridge-timeouts.js +118 -0
- package/dist/bridge-timeouts.js.map +1 -0
- package/dist/bridge.js +10 -2
- package/dist/bridge.js.map +1 -1
- package/dist/editor-control.d.ts +72 -5
- package/dist/editor-control.js +164 -140
- package/dist/editor-control.js.map +1 -1
- package/dist/editor-target.d.ts +55 -1
- package/dist/editor-target.js +107 -10
- package/dist/editor-target.js.map +1 -1
- package/dist/engine-observer.d.ts +59 -5
- package/dist/engine-observer.js +249 -29
- package/dist/engine-observer.js.map +1 -1
- package/dist/engine-root.d.ts +91 -0
- package/dist/engine-root.js +448 -0
- package/dist/engine-root.js.map +1 -0
- package/dist/epic-enrich.js +16 -1
- package/dist/epic-enrich.js.map +1 -1
- package/dist/feedback-cli.js +4 -0
- package/dist/feedback-cli.js.map +1 -1
- package/dist/feedback-deferred.d.ts +9 -0
- package/dist/feedback-deferred.js +2 -0
- package/dist/feedback-deferred.js.map +1 -1
- package/dist/feedback-fallback.d.ts +99 -0
- package/dist/feedback-fallback.js +173 -0
- package/dist/feedback-fallback.js.map +1 -0
- package/dist/flow/write-methods.js +20 -0
- package/dist/flow/write-methods.js.map +1 -1
- package/dist/index.js +41 -2
- package/dist/index.js.map +1 -1
- package/dist/lean-context.js +8 -3
- package/dist/lean-context.js.map +1 -1
- package/dist/locking.js +11 -2
- package/dist/locking.js.map +1 -1
- package/dist/plugin/loader.d.ts +4 -0
- package/dist/plugin/loader.js +22 -8
- package/dist/plugin/loader.js.map +1 -1
- package/dist/plugin/manifest.d.ts +72 -52
- package/dist/plugin/manifest.js +118 -21
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin-cli.js +16 -3
- package/dist/plugin-cli.js.map +1 -1
- package/dist/project.d.ts +11 -0
- package/dist/project.js +23 -2
- package/dist/project.js.map +1 -1
- package/dist/python-gate.d.ts +95 -0
- package/dist/python-gate.js +175 -0
- package/dist/python-gate.js.map +1 -0
- package/dist/tool-counts.json +16 -16
- package/dist/tools/animation.js +288 -24
- package/dist/tools/animation.js.map +1 -1
- package/dist/tools/asset.js +79 -14
- package/dist/tools/asset.js.map +1 -1
- package/dist/tools/audio.js +59 -5
- package/dist/tools/audio.js.map +1 -1
- package/dist/tools/blueprint.js +29 -12
- package/dist/tools/blueprint.js.map +1 -1
- package/dist/tools/chooser.js +4 -1
- package/dist/tools/chooser.js.map +1 -1
- package/dist/tools/editor.js +81 -43
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/feedback.js +214 -48
- package/dist/tools/feedback.js.map +1 -1
- package/dist/tools/foliage.js +27 -1
- package/dist/tools/foliage.js.map +1 -1
- package/dist/tools/gameplay.js +38 -11
- package/dist/tools/gameplay.js.map +1 -1
- package/dist/tools/gas.js +17 -5
- package/dist/tools/gas.js.map +1 -1
- package/dist/tools/landscape.js +22 -7
- package/dist/tools/landscape.js.map +1 -1
- package/dist/tools/level.js +322 -51
- package/dist/tools/level.js.map +1 -1
- package/dist/tools/material.js +10 -4
- package/dist/tools/material.js.map +1 -1
- package/dist/tools/niagara.js +3 -2
- package/dist/tools/niagara.js.map +1 -1
- package/dist/tools/pcg.js +6 -5
- package/dist/tools/pcg.js.map +1 -1
- package/dist/tools/plugins.js +2 -0
- package/dist/tools/plugins.js.map +1 -1
- package/dist/tools/project.js +47 -20
- package/dist/tools/project.js.map +1 -1
- package/dist/types.d.ts +24 -0
- package/dist/types.js +46 -3
- package/dist/types.js.map +1 -1
- package/dist/ue-mcp.default.yml +493 -125
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
- package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
- package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
- package/skills/ue-mcp-animation/SKILL.md +154 -0
- 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
|
+
}
|