ue-mcp 1.2.1 → 1.2.2
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/dist/context-cli.js +16 -16
- package/dist/engine-observer.js +68 -68
- package/dist/flow/guard.d.ts +26 -37
- package/dist/flow/guard.js +40 -25
- package/dist/flow/guard.js.map +1 -1
- package/dist/flow/guarded-bridge.d.ts +0 -10
- package/dist/flow/guarded-bridge.js +18 -23
- package/dist/flow/guarded-bridge.js.map +1 -1
- package/dist/flow/schema.d.ts +276 -16
- package/dist/flow/task-guards.d.ts +1 -1
- package/dist/flow/task-guards.js +44 -68
- package/dist/flow/task-guards.js.map +1 -1
- package/dist/instructions.js +130 -130
- package/dist/plugin-cli.js +299 -299
- package/dist/tool-counts.json +2 -2
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.cpp +76 -76
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.h +27 -27
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.cpp +128 -128
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.h +63 -63
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +230 -230
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +323 -323
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +20 -20
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ChooserHandlers_Nested.cpp +537 -537
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.h +77 -77
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +353 -353
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +18 -18
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_InputRuntime.cpp +246 -246
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +597 -597
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +51 -51
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +878 -878
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +449 -449
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Function.cpp +275 -275
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Graph.cpp +551 -551
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.cpp +531 -531
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.h +28 -28
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +677 -677
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +822 -822
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +22 -22
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +218 -218
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.h +17 -17
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.cpp +46 -46
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.h +23 -23
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/MCPExternalRegistry.cpp +83 -83
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/UE_MCP_BridgeModule.h +14 -14
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/UE_MCP_BridgeStatusModule.cpp +37 -37
- package/dist/manifest-signature.d.ts +0 -1
- package/dist/manifest-signature.js +0 -53
- package/dist/manifest-signature.js.map +0 -1
- package/dist/pie/schema.d.ts +0 -791
- package/dist/pie/schema.js +0 -151
- package/dist/pie/schema.js.map +0 -1
- package/dist/watch-cli.js.map +0 -1
- /package/plugin/ue_mcp_bridge/{ue_mcp_bridge.uplugin → UE_MCP_Bridge.uplugin} +0 -0
package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Graph.cpp
CHANGED
|
@@ -1,551 +1,551 @@
|
|
|
1
|
-
// Split from MaterialHandlers.cpp to keep that file under 3k lines.
|
|
2
|
-
// All functions below are still members of FMaterialHandlers - this file is a
|
|
3
|
-
// translation-unit partition, not a new class. Handler registration
|
|
4
|
-
// stays in MaterialHandlers.cpp::RegisterHandlers.
|
|
5
|
-
|
|
6
|
-
#include "MaterialHandlers.h"
|
|
7
|
-
#include "HandlerRegistry.h"
|
|
8
|
-
#include "HandlerUtils.h"
|
|
9
|
-
#include "HandlerJsonProperty.h"
|
|
10
|
-
#include "JsonSerializer.h"
|
|
11
|
-
#include "Materials/Material.h"
|
|
12
|
-
#include "Materials/MaterialInstanceConstant.h"
|
|
13
|
-
#include "Materials/MaterialExpressionTextureSample.h"
|
|
14
|
-
#include "Materials/MaterialExpressionConstant.h"
|
|
15
|
-
#include "Materials/MaterialExpressionConstant3Vector.h"
|
|
16
|
-
#include "Materials/MaterialExpressionConstant4Vector.h"
|
|
17
|
-
#include "Materials/MaterialExpressionScalarParameter.h"
|
|
18
|
-
#include "Materials/MaterialExpressionVectorParameter.h"
|
|
19
|
-
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
|
20
|
-
#include "Materials/MaterialExpressionMultiply.h"
|
|
21
|
-
#include "Materials/MaterialExpressionAdd.h"
|
|
22
|
-
#include "Materials/MaterialExpressionLinearInterpolate.h"
|
|
23
|
-
#include "MaterialEditingLibrary.h"
|
|
24
|
-
#include "Engine/Texture.h"
|
|
25
|
-
#include "Engine/Texture2D.h"
|
|
26
|
-
#include "UObject/Package.h"
|
|
27
|
-
#include "Misc/PackageName.h"
|
|
28
|
-
#include "UObject/SavePackage.h"
|
|
29
|
-
#include "Dom/JsonObject.h"
|
|
30
|
-
#include "Dom/JsonValue.h"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
TSharedPtr<FJsonValue> FMaterialHandlers::ConnectTextureToMaterial(const TSharedPtr<FJsonObject>& Params)
|
|
34
|
-
{
|
|
35
|
-
FString MaterialPath;
|
|
36
|
-
if (auto Err = RequireStringAlt(Params, TEXT("materialPath"), TEXT("path"), MaterialPath)) return Err;
|
|
37
|
-
if (MaterialPath.IsEmpty())
|
|
38
|
-
{
|
|
39
|
-
Params->TryGetStringField(TEXT("assetPath"), MaterialPath);
|
|
40
|
-
if (MaterialPath.IsEmpty())
|
|
41
|
-
{
|
|
42
|
-
return MCPError(TEXT("Missing required parameter 'materialPath' (or 'path')"));
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
FString TexturePath;
|
|
47
|
-
if (auto Err = RequireString(Params, TEXT("texturePath"), TexturePath)) return Err;
|
|
48
|
-
|
|
49
|
-
FString PropertyName = TEXT("BaseColor");
|
|
50
|
-
if (!Params->TryGetStringField(TEXT("property"), PropertyName))
|
|
51
|
-
{
|
|
52
|
-
Params->TryGetStringField(TEXT("materialProperty"), PropertyName);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
UMaterial* Material = LoadMaterialFromPath(MaterialPath);
|
|
56
|
-
if (!Material)
|
|
57
|
-
{
|
|
58
|
-
return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *MaterialPath));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Load the texture
|
|
62
|
-
UTexture* Texture = Cast<UTexture>(StaticLoadObject(UTexture::StaticClass(), nullptr, *TexturePath));
|
|
63
|
-
if (!Texture)
|
|
64
|
-
{
|
|
65
|
-
Texture = Cast<UTexture>(StaticLoadObject(UTexture::StaticClass(), nullptr,
|
|
66
|
-
*(TEXT("Texture2D'") + TexturePath + TEXT("'"))));
|
|
67
|
-
}
|
|
68
|
-
if (!Texture)
|
|
69
|
-
{
|
|
70
|
-
return MCPError(FString::Printf(TEXT("Failed to load texture at '%s'"), *TexturePath));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
EMaterialProperty MatProperty;
|
|
74
|
-
if (!ParseMaterialProperty(PropertyName, MatProperty))
|
|
75
|
-
{
|
|
76
|
-
return MCPError(FString::Printf(TEXT("Unknown material property '%s'"), *PropertyName));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
Material->PreEditChange(nullptr);
|
|
80
|
-
|
|
81
|
-
// Create a TextureSample expression.
|
|
82
|
-
// Note: connect_texture_to_material adds a new TextureSample node every call
|
|
83
|
-
// (not natural-key idempotent). Use connect_material_expressions with named
|
|
84
|
-
// source/target expressions if idempotency is required.
|
|
85
|
-
UMaterialExpressionTextureSample* TextureSampleExpr = NewObject<UMaterialExpressionTextureSample>(Material);
|
|
86
|
-
TextureSampleExpr->Texture = Texture;
|
|
87
|
-
TextureSampleExpr->MaterialExpressionEditorX = -400;
|
|
88
|
-
TextureSampleExpr->MaterialExpressionEditorY = 0;
|
|
89
|
-
|
|
90
|
-
Material->GetExpressionCollection().AddExpression(TextureSampleExpr);
|
|
91
|
-
|
|
92
|
-
// Connect RGB output (index 0) to the requested material property
|
|
93
|
-
UMaterialEditorOnlyData* EditorOnlyData = Material->GetEditorOnlyData();
|
|
94
|
-
if (FExpressionInput* PropertyInput = GetMaterialPropertyInput(EditorOnlyData, MatProperty))
|
|
95
|
-
{
|
|
96
|
-
PropertyInput->Connect(0, TextureSampleExpr);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
Material->PostEditChange();
|
|
100
|
-
Material->MarkPackageDirty();
|
|
101
|
-
|
|
102
|
-
auto Result = MCPSuccess();
|
|
103
|
-
MCPSetUpdated(Result);
|
|
104
|
-
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
105
|
-
Result->SetStringField(TEXT("texturePath"), Texture->GetPathName());
|
|
106
|
-
Result->SetStringField(TEXT("property"), PropertyName);
|
|
107
|
-
Result->SetNumberField(TEXT("expressionCount"), Material->GetExpressions().Num());
|
|
108
|
-
|
|
109
|
-
return MCPResult(Result);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
TSharedPtr<FJsonValue> FMaterialHandlers::ConnectMaterialExpressions(const TSharedPtr<FJsonObject>& Params)
|
|
114
|
-
{
|
|
115
|
-
FString MaterialPath;
|
|
116
|
-
if (auto Err = RequireStringAlt(Params, TEXT("materialPath"), TEXT("path"), MaterialPath)) return Err;
|
|
117
|
-
if (MaterialPath.IsEmpty())
|
|
118
|
-
{
|
|
119
|
-
Params->TryGetStringField(TEXT("assetPath"), MaterialPath);
|
|
120
|
-
if (MaterialPath.IsEmpty())
|
|
121
|
-
{
|
|
122
|
-
return MCPError(TEXT("Missing required parameter 'materialPath' (or 'path')"));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
FString SourceExpressionName;
|
|
127
|
-
if (auto Err = RequireString(Params, TEXT("sourceExpression"), SourceExpressionName)) return Err;
|
|
128
|
-
|
|
129
|
-
FString TargetExpressionName;
|
|
130
|
-
if (auto Err = RequireString(Params, TEXT("targetExpression"), TargetExpressionName)) return Err;
|
|
131
|
-
|
|
132
|
-
// Source/target output/input can be specified by name or index
|
|
133
|
-
FString SourceOutputName = OptionalString(Params, TEXT("sourceOutput"));
|
|
134
|
-
FString TargetInputName = OptionalString(Params, TEXT("targetInput"));
|
|
135
|
-
|
|
136
|
-
UMaterial* Material = LoadMaterialFromPath(MaterialPath);
|
|
137
|
-
if (!Material)
|
|
138
|
-
{
|
|
139
|
-
return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *MaterialPath));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
UMaterialExpression* SourceExpression = FindExpressionByName(Material, SourceExpressionName);
|
|
143
|
-
if (!SourceExpression)
|
|
144
|
-
{
|
|
145
|
-
return MCPError(FString::Printf(TEXT("Source expression '%s' not found"), *SourceExpressionName));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
UMaterialExpression* TargetExpression = FindExpressionByName(Material, TargetExpressionName);
|
|
149
|
-
if (!TargetExpression)
|
|
150
|
-
{
|
|
151
|
-
return MCPError(FString::Printf(TEXT("Target expression '%s' not found"), *TargetExpressionName));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Resolve source output index. Track whether we matched a named pin so
|
|
155
|
-
// unknown names fail loudly instead of silently aliasing to index 0
|
|
156
|
-
// and overwriting whatever connection lives there (#318).
|
|
157
|
-
int32 SourceOutputIndex = 0;
|
|
158
|
-
bool bSourceOutputResolved = SourceOutputName.IsEmpty(); // empty == "use default 0"
|
|
159
|
-
if (!SourceOutputName.IsEmpty())
|
|
160
|
-
{
|
|
161
|
-
if (SourceOutputName.IsNumeric())
|
|
162
|
-
{
|
|
163
|
-
SourceOutputIndex = FCString::Atoi(*SourceOutputName);
|
|
164
|
-
bSourceOutputResolved = true;
|
|
165
|
-
}
|
|
166
|
-
else
|
|
167
|
-
{
|
|
168
|
-
TArray<FExpressionOutput>& Outputs = SourceExpression->GetOutputs();
|
|
169
|
-
for (int32 i = 0; i < Outputs.Num(); i++)
|
|
170
|
-
{
|
|
171
|
-
if (Outputs[i].OutputName.ToString().Equals(SourceOutputName, ESearchCase::IgnoreCase))
|
|
172
|
-
{
|
|
173
|
-
SourceOutputIndex = i;
|
|
174
|
-
bSourceOutputResolved = true;
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (!bSourceOutputResolved)
|
|
179
|
-
{
|
|
180
|
-
TArray<FString> Names;
|
|
181
|
-
for (const FExpressionOutput& O : Outputs) { Names.Add(O.OutputName.ToString()); }
|
|
182
|
-
return MCPError(FString::Printf(
|
|
183
|
-
TEXT("Source output '%s' not found on '%s'. Available: [%s]"),
|
|
184
|
-
*SourceOutputName, *SourceExpressionName,
|
|
185
|
-
*FString::Join(Names, TEXT(", "))));
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Resolve target input index. Same loud-fail rule (#318) - this is the
|
|
191
|
-
// case that previously aliased unknown names to A/RGB and clobbered prior
|
|
192
|
-
// wiring.
|
|
193
|
-
int32 TargetInputIndex = 0;
|
|
194
|
-
bool bTargetInputResolved = TargetInputName.IsEmpty();
|
|
195
|
-
if (!TargetInputName.IsEmpty())
|
|
196
|
-
{
|
|
197
|
-
if (TargetInputName.IsNumeric())
|
|
198
|
-
{
|
|
199
|
-
TargetInputIndex = FCString::Atoi(*TargetInputName);
|
|
200
|
-
bTargetInputResolved = true;
|
|
201
|
-
}
|
|
202
|
-
else
|
|
203
|
-
{
|
|
204
|
-
for (int32 i = 0; ; i++)
|
|
205
|
-
{
|
|
206
|
-
FExpressionInput* Input = TargetExpression->GetInput(i);
|
|
207
|
-
if (!Input) break;
|
|
208
|
-
FName InputName = TargetExpression->GetInputName(i);
|
|
209
|
-
if (InputName.ToString().Equals(TargetInputName, ESearchCase::IgnoreCase))
|
|
210
|
-
{
|
|
211
|
-
TargetInputIndex = i;
|
|
212
|
-
bTargetInputResolved = true;
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (!bTargetInputResolved)
|
|
217
|
-
{
|
|
218
|
-
TArray<FString> Names;
|
|
219
|
-
for (int32 i = 0; ; i++)
|
|
220
|
-
{
|
|
221
|
-
if (!TargetExpression->GetInput(i)) break;
|
|
222
|
-
Names.Add(TargetExpression->GetInputName(i).ToString());
|
|
223
|
-
}
|
|
224
|
-
return MCPError(FString::Printf(
|
|
225
|
-
TEXT("Target input '%s' not found on '%s'. Available: [%s]"),
|
|
226
|
-
*TargetInputName, *TargetExpressionName,
|
|
227
|
-
*FString::Join(Names, TEXT(", "))));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
FExpressionInput* TargetInput = TargetExpression->GetInput(TargetInputIndex);
|
|
233
|
-
if (!TargetInput)
|
|
234
|
-
{
|
|
235
|
-
return MCPError(FString::Printf(TEXT("Target input index %d is out of range"), TargetInputIndex));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Idempotency: already wired?
|
|
239
|
-
if (TargetInput->Expression == SourceExpression && TargetInput->OutputIndex == SourceOutputIndex)
|
|
240
|
-
{
|
|
241
|
-
auto Existed = MCPSuccess();
|
|
242
|
-
MCPSetExisted(Existed);
|
|
243
|
-
Existed->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
244
|
-
Existed->SetStringField(TEXT("sourceExpression"), SourceExpressionName);
|
|
245
|
-
Existed->SetStringField(TEXT("targetExpression"), TargetExpressionName);
|
|
246
|
-
Existed->SetNumberField(TEXT("sourceOutputIndex"), SourceOutputIndex);
|
|
247
|
-
Existed->SetNumberField(TEXT("targetInputIndex"), TargetInputIndex);
|
|
248
|
-
return MCPResult(Existed);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
Material->PreEditChange(nullptr);
|
|
252
|
-
TargetInput->Connect(SourceOutputIndex, SourceExpression);
|
|
253
|
-
Material->PostEditChange();
|
|
254
|
-
Material->MarkPackageDirty();
|
|
255
|
-
|
|
256
|
-
auto Result = MCPSuccess();
|
|
257
|
-
MCPSetUpdated(Result);
|
|
258
|
-
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
259
|
-
Result->SetStringField(TEXT("sourceExpression"), SourceExpression->GetClass()->GetName());
|
|
260
|
-
Result->SetStringField(TEXT("targetExpression"), TargetExpression->GetClass()->GetName());
|
|
261
|
-
Result->SetNumberField(TEXT("sourceOutputIndex"), SourceOutputIndex);
|
|
262
|
-
Result->SetNumberField(TEXT("targetInputIndex"), TargetInputIndex);
|
|
263
|
-
// No rollback: no paired disconnect handler by names.
|
|
264
|
-
|
|
265
|
-
return MCPResult(Result);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
TSharedPtr<FJsonValue> FMaterialHandlers::ConnectToMaterialProperty(const TSharedPtr<FJsonObject>& Params)
|
|
270
|
-
{
|
|
271
|
-
FString MaterialPath;
|
|
272
|
-
if (auto Err = RequireStringAlt(Params, TEXT("materialPath"), TEXT("path"), MaterialPath)) return Err;
|
|
273
|
-
if (MaterialPath.IsEmpty())
|
|
274
|
-
{
|
|
275
|
-
Params->TryGetStringField(TEXT("assetPath"), MaterialPath);
|
|
276
|
-
if (MaterialPath.IsEmpty())
|
|
277
|
-
{
|
|
278
|
-
return MCPError(TEXT("Missing required parameter 'materialPath' (or 'path')"));
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
FString ExpressionName;
|
|
283
|
-
if (auto Err = RequireString(Params, TEXT("expressionName"), ExpressionName)) return Err;
|
|
284
|
-
|
|
285
|
-
FString PropertyName;
|
|
286
|
-
if (auto Err = RequireString(Params, TEXT("property"), PropertyName)) return Err;
|
|
287
|
-
|
|
288
|
-
FString OutputName = OptionalString(Params, TEXT("outputName"));
|
|
289
|
-
|
|
290
|
-
UMaterial* Material = LoadMaterialFromPath(MaterialPath);
|
|
291
|
-
if (!Material)
|
|
292
|
-
{
|
|
293
|
-
return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *MaterialPath));
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
UMaterialExpression* Expression = FindExpressionByName(Material, ExpressionName);
|
|
297
|
-
if (!Expression)
|
|
298
|
-
{
|
|
299
|
-
return MCPError(FString::Printf(TEXT("Expression '%s' not found"), *ExpressionName));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Resolve output index
|
|
303
|
-
int32 OutputIndex = 0;
|
|
304
|
-
if (!OutputName.IsEmpty())
|
|
305
|
-
{
|
|
306
|
-
if (OutputName.IsNumeric())
|
|
307
|
-
{
|
|
308
|
-
OutputIndex = FCString::Atoi(*OutputName);
|
|
309
|
-
}
|
|
310
|
-
else
|
|
311
|
-
{
|
|
312
|
-
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
|
|
313
|
-
for (int32 i = 0; i < Outputs.Num(); i++)
|
|
314
|
-
{
|
|
315
|
-
if (Outputs[i].OutputName.ToString().Equals(OutputName, ESearchCase::IgnoreCase))
|
|
316
|
-
{
|
|
317
|
-
OutputIndex = i;
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
EMaterialProperty MatProperty;
|
|
325
|
-
if (!ParseMaterialProperty(PropertyName, MatProperty))
|
|
326
|
-
{
|
|
327
|
-
return MCPError(FString::Printf(TEXT("Unknown material property '%s'"), *PropertyName));
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
Material->PreEditChange(nullptr);
|
|
331
|
-
|
|
332
|
-
UMaterialEditorOnlyData* EditorOnlyData = Material->GetEditorOnlyData();
|
|
333
|
-
if (!EditorOnlyData)
|
|
334
|
-
{
|
|
335
|
-
return MCPError(TEXT("Material has no editor-only data (is this material domain supported?)"));
|
|
336
|
-
}
|
|
337
|
-
FExpressionInput* PropertyInput = GetMaterialPropertyInput(EditorOnlyData, MatProperty);
|
|
338
|
-
if (!PropertyInput)
|
|
339
|
-
{
|
|
340
|
-
return MCPError(FString::Printf(TEXT("Material property '%s' is not supported for direct connection"), *PropertyName));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Idempotency
|
|
344
|
-
if (PropertyInput->Expression == Expression && PropertyInput->OutputIndex == OutputIndex)
|
|
345
|
-
{
|
|
346
|
-
auto Existed = MCPSuccess();
|
|
347
|
-
MCPSetExisted(Existed);
|
|
348
|
-
Existed->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
349
|
-
Existed->SetStringField(TEXT("expressionName"), ExpressionName);
|
|
350
|
-
Existed->SetStringField(TEXT("property"), PropertyName);
|
|
351
|
-
Existed->SetNumberField(TEXT("outputIndex"), OutputIndex);
|
|
352
|
-
return MCPResult(Existed);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
PropertyInput->Connect(OutputIndex, Expression);
|
|
356
|
-
|
|
357
|
-
Material->PostEditChange();
|
|
358
|
-
Material->MarkPackageDirty();
|
|
359
|
-
|
|
360
|
-
auto Result = MCPSuccess();
|
|
361
|
-
MCPSetUpdated(Result);
|
|
362
|
-
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
363
|
-
Result->SetStringField(TEXT("expressionName"), ExpressionName);
|
|
364
|
-
Result->SetStringField(TEXT("expressionClass"), Expression->GetClass()->GetName());
|
|
365
|
-
Result->SetStringField(TEXT("property"), PropertyName);
|
|
366
|
-
Result->SetNumberField(TEXT("outputIndex"), OutputIndex);
|
|
367
|
-
|
|
368
|
-
// Rollback: disconnect_material_property
|
|
369
|
-
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
370
|
-
Payload->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
371
|
-
Payload->SetStringField(TEXT("property"), PropertyName);
|
|
372
|
-
MCPSetRollback(Result, TEXT("disconnect_material_property"), Payload);
|
|
373
|
-
|
|
374
|
-
return MCPResult(Result);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
TSharedPtr<FJsonValue> FMaterialHandlers::DeleteMaterialExpression(const TSharedPtr<FJsonObject>& Params)
|
|
379
|
-
{
|
|
380
|
-
FString MaterialPath;
|
|
381
|
-
if (auto Err = RequireStringAlt(Params, TEXT("materialPath"), TEXT("path"), MaterialPath)) return Err;
|
|
382
|
-
if (MaterialPath.IsEmpty())
|
|
383
|
-
{
|
|
384
|
-
Params->TryGetStringField(TEXT("assetPath"), MaterialPath);
|
|
385
|
-
if (MaterialPath.IsEmpty())
|
|
386
|
-
{
|
|
387
|
-
return MCPError(TEXT("Missing required parameter 'materialPath' (or 'path')"));
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
FString ExpressionName;
|
|
392
|
-
if (auto Err = RequireString(Params, TEXT("expressionName"), ExpressionName)) return Err;
|
|
393
|
-
|
|
394
|
-
UMaterial* Material = LoadMaterialFromPath(MaterialPath);
|
|
395
|
-
if (!Material)
|
|
396
|
-
{
|
|
397
|
-
return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *MaterialPath));
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
UMaterialExpression* Expression = FindExpressionByName(Material, ExpressionName);
|
|
401
|
-
if (!Expression)
|
|
402
|
-
{
|
|
403
|
-
// Idempotent: already deleted
|
|
404
|
-
auto Noop = MCPSuccess();
|
|
405
|
-
Noop->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
406
|
-
Noop->SetStringField(TEXT("expressionName"), ExpressionName);
|
|
407
|
-
Noop->SetBoolField(TEXT("alreadyDeleted"), true);
|
|
408
|
-
return MCPResult(Noop);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
FString DeletedClass = Expression->GetClass()->GetName();
|
|
412
|
-
|
|
413
|
-
Material->PreEditChange(nullptr);
|
|
414
|
-
|
|
415
|
-
// Disconnect all references from other expressions that point to this one
|
|
416
|
-
for (UMaterialExpression* OtherExpr : Material->GetExpressions())
|
|
417
|
-
{
|
|
418
|
-
if (!OtherExpr || OtherExpr == Expression) continue;
|
|
419
|
-
for (int32 i = 0; ; i++)
|
|
420
|
-
{
|
|
421
|
-
FExpressionInput* Input = OtherExpr->GetInput(i);
|
|
422
|
-
if (!Input) break;
|
|
423
|
-
if (Input->Expression == Expression)
|
|
424
|
-
{
|
|
425
|
-
Input->Expression = nullptr;
|
|
426
|
-
Input->OutputIndex = 0;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Disconnect any material property inputs that reference this expression
|
|
432
|
-
UMaterialEditorOnlyData* EditorOnlyData = Material->GetEditorOnlyData();
|
|
433
|
-
if (EditorOnlyData)
|
|
434
|
-
{
|
|
435
|
-
auto ClearIfMatch = [Expression](FExpressionInput& Input)
|
|
436
|
-
{
|
|
437
|
-
if (Input.Expression == Expression)
|
|
438
|
-
{
|
|
439
|
-
Input.Expression = nullptr;
|
|
440
|
-
Input.OutputIndex = 0;
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
ClearIfMatch(EditorOnlyData->BaseColor);
|
|
444
|
-
ClearIfMatch(EditorOnlyData->Metallic);
|
|
445
|
-
ClearIfMatch(EditorOnlyData->Specular);
|
|
446
|
-
ClearIfMatch(EditorOnlyData->Roughness);
|
|
447
|
-
ClearIfMatch(EditorOnlyData->Anisotropy);
|
|
448
|
-
ClearIfMatch(EditorOnlyData->EmissiveColor);
|
|
449
|
-
ClearIfMatch(EditorOnlyData->Opacity);
|
|
450
|
-
ClearIfMatch(EditorOnlyData->OpacityMask);
|
|
451
|
-
ClearIfMatch(EditorOnlyData->Normal);
|
|
452
|
-
ClearIfMatch(EditorOnlyData->Tangent);
|
|
453
|
-
ClearIfMatch(EditorOnlyData->WorldPositionOffset);
|
|
454
|
-
ClearIfMatch(EditorOnlyData->SubsurfaceColor);
|
|
455
|
-
ClearIfMatch(EditorOnlyData->AmbientOcclusion);
|
|
456
|
-
ClearIfMatch(EditorOnlyData->Refraction);
|
|
457
|
-
ClearIfMatch(EditorOnlyData->PixelDepthOffset);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
Material->GetExpressionCollection().RemoveExpression(Expression);
|
|
461
|
-
Material->PostEditChange();
|
|
462
|
-
Material->MarkPackageDirty();
|
|
463
|
-
|
|
464
|
-
auto Result = MCPSuccess();
|
|
465
|
-
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
466
|
-
Result->SetStringField(TEXT("deletedExpression"), ExpressionName);
|
|
467
|
-
Result->SetStringField(TEXT("deletedClass"), DeletedClass);
|
|
468
|
-
Result->SetNumberField(TEXT("expressionCount"), Material->GetExpressions().Num());
|
|
469
|
-
Result->SetBoolField(TEXT("deleted"), true);
|
|
470
|
-
// No rollback: would require snapshotting the expression and all its connections.
|
|
471
|
-
|
|
472
|
-
return MCPResult(Result);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// ---------------------------------------------------------------------------
|
|
476
|
-
// disconnect_material_property -- Clear a material property input (#43)
|
|
477
|
-
// Params: materialPath, property (BaseColor, Normal, Roughness, etc.)
|
|
478
|
-
// ---------------------------------------------------------------------------
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
// ---------------------------------------------------------------------------
|
|
482
|
-
// disconnect_material_property -- Clear a material property input (#43)
|
|
483
|
-
// Params: materialPath, property (BaseColor, Normal, Roughness, etc.)
|
|
484
|
-
// ---------------------------------------------------------------------------
|
|
485
|
-
TSharedPtr<FJsonValue> FMaterialHandlers::DisconnectMaterialProperty(const TSharedPtr<FJsonObject>& Params)
|
|
486
|
-
{
|
|
487
|
-
FString MaterialPath;
|
|
488
|
-
if (auto Err = RequireStringAlt(Params, TEXT("materialPath"), TEXT("assetPath"), MaterialPath)) return Err;
|
|
489
|
-
|
|
490
|
-
FString PropertyName;
|
|
491
|
-
if (auto Err = RequireString(Params, TEXT("property"), PropertyName)) return Err;
|
|
492
|
-
|
|
493
|
-
UMaterial* Material = LoadMaterialFromPath(MaterialPath);
|
|
494
|
-
if (!Material)
|
|
495
|
-
{
|
|
496
|
-
return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *MaterialPath));
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
UMaterialEditorOnlyData* EditorOnlyData = Material->GetEditorOnlyData();
|
|
500
|
-
if (!EditorOnlyData)
|
|
501
|
-
{
|
|
502
|
-
return MCPError(TEXT("Material has no editor-only data"));
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
Material->PreEditChange(nullptr);
|
|
506
|
-
|
|
507
|
-
auto ClearInput = [](FExpressionInput& Input)
|
|
508
|
-
{
|
|
509
|
-
Input.Expression = nullptr;
|
|
510
|
-
Input.OutputIndex = 0;
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
FString LowerProp = PropertyName.ToLower();
|
|
514
|
-
bool bFound = true;
|
|
515
|
-
|
|
516
|
-
if (LowerProp == TEXT("basecolor")) ClearInput(EditorOnlyData->BaseColor);
|
|
517
|
-
else if (LowerProp == TEXT("metallic")) ClearInput(EditorOnlyData->Metallic);
|
|
518
|
-
else if (LowerProp == TEXT("specular")) ClearInput(EditorOnlyData->Specular);
|
|
519
|
-
else if (LowerProp == TEXT("roughness")) ClearInput(EditorOnlyData->Roughness);
|
|
520
|
-
else if (LowerProp == TEXT("anisotropy")) ClearInput(EditorOnlyData->Anisotropy);
|
|
521
|
-
else if (LowerProp == TEXT("emissivecolor") || LowerProp == TEXT("emissive")) ClearInput(EditorOnlyData->EmissiveColor);
|
|
522
|
-
else if (LowerProp == TEXT("opacity")) ClearInput(EditorOnlyData->Opacity);
|
|
523
|
-
else if (LowerProp == TEXT("opacitymask")) ClearInput(EditorOnlyData->OpacityMask);
|
|
524
|
-
else if (LowerProp == TEXT("normal")) ClearInput(EditorOnlyData->Normal);
|
|
525
|
-
else if (LowerProp == TEXT("tangent")) ClearInput(EditorOnlyData->Tangent);
|
|
526
|
-
else if (LowerProp == TEXT("worldpositionoffset")) ClearInput(EditorOnlyData->WorldPositionOffset);
|
|
527
|
-
else if (LowerProp == TEXT("subsurfacecolor")) ClearInput(EditorOnlyData->SubsurfaceColor);
|
|
528
|
-
else if (LowerProp == TEXT("ambientocclusion")) ClearInput(EditorOnlyData->AmbientOcclusion);
|
|
529
|
-
else if (LowerProp == TEXT("refraction")) ClearInput(EditorOnlyData->Refraction);
|
|
530
|
-
else if (LowerProp == TEXT("pixeldepthoffset")) ClearInput(EditorOnlyData->PixelDepthOffset);
|
|
531
|
-
else bFound = false;
|
|
532
|
-
|
|
533
|
-
if (!bFound)
|
|
534
|
-
{
|
|
535
|
-
return MCPError(FString::Printf(
|
|
536
|
-
TEXT("Unknown property '%s'. Use: BaseColor, Metallic, Specular, Roughness, EmissiveColor, Opacity, OpacityMask, Normal, Tangent, WorldPositionOffset, SubsurfaceColor, AmbientOcclusion, Refraction, PixelDepthOffset"),
|
|
537
|
-
*PropertyName));
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
Material->PostEditChange();
|
|
541
|
-
Material->MarkPackageDirty();
|
|
542
|
-
|
|
543
|
-
auto Result = MCPSuccess();
|
|
544
|
-
MCPSetUpdated(Result);
|
|
545
|
-
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
546
|
-
Result->SetStringField(TEXT("property"), PropertyName);
|
|
547
|
-
// No rollback: we don't capture the previous expression binding before clearing.
|
|
548
|
-
|
|
549
|
-
return MCPResult(Result);
|
|
550
|
-
}
|
|
551
|
-
|
|
1
|
+
// Split from MaterialHandlers.cpp to keep that file under 3k lines.
|
|
2
|
+
// All functions below are still members of FMaterialHandlers - this file is a
|
|
3
|
+
// translation-unit partition, not a new class. Handler registration
|
|
4
|
+
// stays in MaterialHandlers.cpp::RegisterHandlers.
|
|
5
|
+
|
|
6
|
+
#include "MaterialHandlers.h"
|
|
7
|
+
#include "HandlerRegistry.h"
|
|
8
|
+
#include "HandlerUtils.h"
|
|
9
|
+
#include "HandlerJsonProperty.h"
|
|
10
|
+
#include "JsonSerializer.h"
|
|
11
|
+
#include "Materials/Material.h"
|
|
12
|
+
#include "Materials/MaterialInstanceConstant.h"
|
|
13
|
+
#include "Materials/MaterialExpressionTextureSample.h"
|
|
14
|
+
#include "Materials/MaterialExpressionConstant.h"
|
|
15
|
+
#include "Materials/MaterialExpressionConstant3Vector.h"
|
|
16
|
+
#include "Materials/MaterialExpressionConstant4Vector.h"
|
|
17
|
+
#include "Materials/MaterialExpressionScalarParameter.h"
|
|
18
|
+
#include "Materials/MaterialExpressionVectorParameter.h"
|
|
19
|
+
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
|
20
|
+
#include "Materials/MaterialExpressionMultiply.h"
|
|
21
|
+
#include "Materials/MaterialExpressionAdd.h"
|
|
22
|
+
#include "Materials/MaterialExpressionLinearInterpolate.h"
|
|
23
|
+
#include "MaterialEditingLibrary.h"
|
|
24
|
+
#include "Engine/Texture.h"
|
|
25
|
+
#include "Engine/Texture2D.h"
|
|
26
|
+
#include "UObject/Package.h"
|
|
27
|
+
#include "Misc/PackageName.h"
|
|
28
|
+
#include "UObject/SavePackage.h"
|
|
29
|
+
#include "Dom/JsonObject.h"
|
|
30
|
+
#include "Dom/JsonValue.h"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
TSharedPtr<FJsonValue> FMaterialHandlers::ConnectTextureToMaterial(const TSharedPtr<FJsonObject>& Params)
|
|
34
|
+
{
|
|
35
|
+
FString MaterialPath;
|
|
36
|
+
if (auto Err = RequireStringAlt(Params, TEXT("materialPath"), TEXT("path"), MaterialPath)) return Err;
|
|
37
|
+
if (MaterialPath.IsEmpty())
|
|
38
|
+
{
|
|
39
|
+
Params->TryGetStringField(TEXT("assetPath"), MaterialPath);
|
|
40
|
+
if (MaterialPath.IsEmpty())
|
|
41
|
+
{
|
|
42
|
+
return MCPError(TEXT("Missing required parameter 'materialPath' (or 'path')"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
FString TexturePath;
|
|
47
|
+
if (auto Err = RequireString(Params, TEXT("texturePath"), TexturePath)) return Err;
|
|
48
|
+
|
|
49
|
+
FString PropertyName = TEXT("BaseColor");
|
|
50
|
+
if (!Params->TryGetStringField(TEXT("property"), PropertyName))
|
|
51
|
+
{
|
|
52
|
+
Params->TryGetStringField(TEXT("materialProperty"), PropertyName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
UMaterial* Material = LoadMaterialFromPath(MaterialPath);
|
|
56
|
+
if (!Material)
|
|
57
|
+
{
|
|
58
|
+
return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *MaterialPath));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Load the texture
|
|
62
|
+
UTexture* Texture = Cast<UTexture>(StaticLoadObject(UTexture::StaticClass(), nullptr, *TexturePath));
|
|
63
|
+
if (!Texture)
|
|
64
|
+
{
|
|
65
|
+
Texture = Cast<UTexture>(StaticLoadObject(UTexture::StaticClass(), nullptr,
|
|
66
|
+
*(TEXT("Texture2D'") + TexturePath + TEXT("'"))));
|
|
67
|
+
}
|
|
68
|
+
if (!Texture)
|
|
69
|
+
{
|
|
70
|
+
return MCPError(FString::Printf(TEXT("Failed to load texture at '%s'"), *TexturePath));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
EMaterialProperty MatProperty;
|
|
74
|
+
if (!ParseMaterialProperty(PropertyName, MatProperty))
|
|
75
|
+
{
|
|
76
|
+
return MCPError(FString::Printf(TEXT("Unknown material property '%s'"), *PropertyName));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Material->PreEditChange(nullptr);
|
|
80
|
+
|
|
81
|
+
// Create a TextureSample expression.
|
|
82
|
+
// Note: connect_texture_to_material adds a new TextureSample node every call
|
|
83
|
+
// (not natural-key idempotent). Use connect_material_expressions with named
|
|
84
|
+
// source/target expressions if idempotency is required.
|
|
85
|
+
UMaterialExpressionTextureSample* TextureSampleExpr = NewObject<UMaterialExpressionTextureSample>(Material);
|
|
86
|
+
TextureSampleExpr->Texture = Texture;
|
|
87
|
+
TextureSampleExpr->MaterialExpressionEditorX = -400;
|
|
88
|
+
TextureSampleExpr->MaterialExpressionEditorY = 0;
|
|
89
|
+
|
|
90
|
+
Material->GetExpressionCollection().AddExpression(TextureSampleExpr);
|
|
91
|
+
|
|
92
|
+
// Connect RGB output (index 0) to the requested material property
|
|
93
|
+
UMaterialEditorOnlyData* EditorOnlyData = Material->GetEditorOnlyData();
|
|
94
|
+
if (FExpressionInput* PropertyInput = GetMaterialPropertyInput(EditorOnlyData, MatProperty))
|
|
95
|
+
{
|
|
96
|
+
PropertyInput->Connect(0, TextureSampleExpr);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Material->PostEditChange();
|
|
100
|
+
Material->MarkPackageDirty();
|
|
101
|
+
|
|
102
|
+
auto Result = MCPSuccess();
|
|
103
|
+
MCPSetUpdated(Result);
|
|
104
|
+
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
105
|
+
Result->SetStringField(TEXT("texturePath"), Texture->GetPathName());
|
|
106
|
+
Result->SetStringField(TEXT("property"), PropertyName);
|
|
107
|
+
Result->SetNumberField(TEXT("expressionCount"), Material->GetExpressions().Num());
|
|
108
|
+
|
|
109
|
+
return MCPResult(Result);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
TSharedPtr<FJsonValue> FMaterialHandlers::ConnectMaterialExpressions(const TSharedPtr<FJsonObject>& Params)
|
|
114
|
+
{
|
|
115
|
+
FString MaterialPath;
|
|
116
|
+
if (auto Err = RequireStringAlt(Params, TEXT("materialPath"), TEXT("path"), MaterialPath)) return Err;
|
|
117
|
+
if (MaterialPath.IsEmpty())
|
|
118
|
+
{
|
|
119
|
+
Params->TryGetStringField(TEXT("assetPath"), MaterialPath);
|
|
120
|
+
if (MaterialPath.IsEmpty())
|
|
121
|
+
{
|
|
122
|
+
return MCPError(TEXT("Missing required parameter 'materialPath' (or 'path')"));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
FString SourceExpressionName;
|
|
127
|
+
if (auto Err = RequireString(Params, TEXT("sourceExpression"), SourceExpressionName)) return Err;
|
|
128
|
+
|
|
129
|
+
FString TargetExpressionName;
|
|
130
|
+
if (auto Err = RequireString(Params, TEXT("targetExpression"), TargetExpressionName)) return Err;
|
|
131
|
+
|
|
132
|
+
// Source/target output/input can be specified by name or index
|
|
133
|
+
FString SourceOutputName = OptionalString(Params, TEXT("sourceOutput"));
|
|
134
|
+
FString TargetInputName = OptionalString(Params, TEXT("targetInput"));
|
|
135
|
+
|
|
136
|
+
UMaterial* Material = LoadMaterialFromPath(MaterialPath);
|
|
137
|
+
if (!Material)
|
|
138
|
+
{
|
|
139
|
+
return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *MaterialPath));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
UMaterialExpression* SourceExpression = FindExpressionByName(Material, SourceExpressionName);
|
|
143
|
+
if (!SourceExpression)
|
|
144
|
+
{
|
|
145
|
+
return MCPError(FString::Printf(TEXT("Source expression '%s' not found"), *SourceExpressionName));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
UMaterialExpression* TargetExpression = FindExpressionByName(Material, TargetExpressionName);
|
|
149
|
+
if (!TargetExpression)
|
|
150
|
+
{
|
|
151
|
+
return MCPError(FString::Printf(TEXT("Target expression '%s' not found"), *TargetExpressionName));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Resolve source output index. Track whether we matched a named pin so
|
|
155
|
+
// unknown names fail loudly instead of silently aliasing to index 0
|
|
156
|
+
// and overwriting whatever connection lives there (#318).
|
|
157
|
+
int32 SourceOutputIndex = 0;
|
|
158
|
+
bool bSourceOutputResolved = SourceOutputName.IsEmpty(); // empty == "use default 0"
|
|
159
|
+
if (!SourceOutputName.IsEmpty())
|
|
160
|
+
{
|
|
161
|
+
if (SourceOutputName.IsNumeric())
|
|
162
|
+
{
|
|
163
|
+
SourceOutputIndex = FCString::Atoi(*SourceOutputName);
|
|
164
|
+
bSourceOutputResolved = true;
|
|
165
|
+
}
|
|
166
|
+
else
|
|
167
|
+
{
|
|
168
|
+
TArray<FExpressionOutput>& Outputs = SourceExpression->GetOutputs();
|
|
169
|
+
for (int32 i = 0; i < Outputs.Num(); i++)
|
|
170
|
+
{
|
|
171
|
+
if (Outputs[i].OutputName.ToString().Equals(SourceOutputName, ESearchCase::IgnoreCase))
|
|
172
|
+
{
|
|
173
|
+
SourceOutputIndex = i;
|
|
174
|
+
bSourceOutputResolved = true;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!bSourceOutputResolved)
|
|
179
|
+
{
|
|
180
|
+
TArray<FString> Names;
|
|
181
|
+
for (const FExpressionOutput& O : Outputs) { Names.Add(O.OutputName.ToString()); }
|
|
182
|
+
return MCPError(FString::Printf(
|
|
183
|
+
TEXT("Source output '%s' not found on '%s'. Available: [%s]"),
|
|
184
|
+
*SourceOutputName, *SourceExpressionName,
|
|
185
|
+
*FString::Join(Names, TEXT(", "))));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Resolve target input index. Same loud-fail rule (#318) - this is the
|
|
191
|
+
// case that previously aliased unknown names to A/RGB and clobbered prior
|
|
192
|
+
// wiring.
|
|
193
|
+
int32 TargetInputIndex = 0;
|
|
194
|
+
bool bTargetInputResolved = TargetInputName.IsEmpty();
|
|
195
|
+
if (!TargetInputName.IsEmpty())
|
|
196
|
+
{
|
|
197
|
+
if (TargetInputName.IsNumeric())
|
|
198
|
+
{
|
|
199
|
+
TargetInputIndex = FCString::Atoi(*TargetInputName);
|
|
200
|
+
bTargetInputResolved = true;
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
{
|
|
204
|
+
for (int32 i = 0; ; i++)
|
|
205
|
+
{
|
|
206
|
+
FExpressionInput* Input = TargetExpression->GetInput(i);
|
|
207
|
+
if (!Input) break;
|
|
208
|
+
FName InputName = TargetExpression->GetInputName(i);
|
|
209
|
+
if (InputName.ToString().Equals(TargetInputName, ESearchCase::IgnoreCase))
|
|
210
|
+
{
|
|
211
|
+
TargetInputIndex = i;
|
|
212
|
+
bTargetInputResolved = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (!bTargetInputResolved)
|
|
217
|
+
{
|
|
218
|
+
TArray<FString> Names;
|
|
219
|
+
for (int32 i = 0; ; i++)
|
|
220
|
+
{
|
|
221
|
+
if (!TargetExpression->GetInput(i)) break;
|
|
222
|
+
Names.Add(TargetExpression->GetInputName(i).ToString());
|
|
223
|
+
}
|
|
224
|
+
return MCPError(FString::Printf(
|
|
225
|
+
TEXT("Target input '%s' not found on '%s'. Available: [%s]"),
|
|
226
|
+
*TargetInputName, *TargetExpressionName,
|
|
227
|
+
*FString::Join(Names, TEXT(", "))));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
FExpressionInput* TargetInput = TargetExpression->GetInput(TargetInputIndex);
|
|
233
|
+
if (!TargetInput)
|
|
234
|
+
{
|
|
235
|
+
return MCPError(FString::Printf(TEXT("Target input index %d is out of range"), TargetInputIndex));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Idempotency: already wired?
|
|
239
|
+
if (TargetInput->Expression == SourceExpression && TargetInput->OutputIndex == SourceOutputIndex)
|
|
240
|
+
{
|
|
241
|
+
auto Existed = MCPSuccess();
|
|
242
|
+
MCPSetExisted(Existed);
|
|
243
|
+
Existed->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
244
|
+
Existed->SetStringField(TEXT("sourceExpression"), SourceExpressionName);
|
|
245
|
+
Existed->SetStringField(TEXT("targetExpression"), TargetExpressionName);
|
|
246
|
+
Existed->SetNumberField(TEXT("sourceOutputIndex"), SourceOutputIndex);
|
|
247
|
+
Existed->SetNumberField(TEXT("targetInputIndex"), TargetInputIndex);
|
|
248
|
+
return MCPResult(Existed);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
Material->PreEditChange(nullptr);
|
|
252
|
+
TargetInput->Connect(SourceOutputIndex, SourceExpression);
|
|
253
|
+
Material->PostEditChange();
|
|
254
|
+
Material->MarkPackageDirty();
|
|
255
|
+
|
|
256
|
+
auto Result = MCPSuccess();
|
|
257
|
+
MCPSetUpdated(Result);
|
|
258
|
+
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
259
|
+
Result->SetStringField(TEXT("sourceExpression"), SourceExpression->GetClass()->GetName());
|
|
260
|
+
Result->SetStringField(TEXT("targetExpression"), TargetExpression->GetClass()->GetName());
|
|
261
|
+
Result->SetNumberField(TEXT("sourceOutputIndex"), SourceOutputIndex);
|
|
262
|
+
Result->SetNumberField(TEXT("targetInputIndex"), TargetInputIndex);
|
|
263
|
+
// No rollback: no paired disconnect handler by names.
|
|
264
|
+
|
|
265
|
+
return MCPResult(Result);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
TSharedPtr<FJsonValue> FMaterialHandlers::ConnectToMaterialProperty(const TSharedPtr<FJsonObject>& Params)
|
|
270
|
+
{
|
|
271
|
+
FString MaterialPath;
|
|
272
|
+
if (auto Err = RequireStringAlt(Params, TEXT("materialPath"), TEXT("path"), MaterialPath)) return Err;
|
|
273
|
+
if (MaterialPath.IsEmpty())
|
|
274
|
+
{
|
|
275
|
+
Params->TryGetStringField(TEXT("assetPath"), MaterialPath);
|
|
276
|
+
if (MaterialPath.IsEmpty())
|
|
277
|
+
{
|
|
278
|
+
return MCPError(TEXT("Missing required parameter 'materialPath' (or 'path')"));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
FString ExpressionName;
|
|
283
|
+
if (auto Err = RequireString(Params, TEXT("expressionName"), ExpressionName)) return Err;
|
|
284
|
+
|
|
285
|
+
FString PropertyName;
|
|
286
|
+
if (auto Err = RequireString(Params, TEXT("property"), PropertyName)) return Err;
|
|
287
|
+
|
|
288
|
+
FString OutputName = OptionalString(Params, TEXT("outputName"));
|
|
289
|
+
|
|
290
|
+
UMaterial* Material = LoadMaterialFromPath(MaterialPath);
|
|
291
|
+
if (!Material)
|
|
292
|
+
{
|
|
293
|
+
return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *MaterialPath));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
UMaterialExpression* Expression = FindExpressionByName(Material, ExpressionName);
|
|
297
|
+
if (!Expression)
|
|
298
|
+
{
|
|
299
|
+
return MCPError(FString::Printf(TEXT("Expression '%s' not found"), *ExpressionName));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Resolve output index
|
|
303
|
+
int32 OutputIndex = 0;
|
|
304
|
+
if (!OutputName.IsEmpty())
|
|
305
|
+
{
|
|
306
|
+
if (OutputName.IsNumeric())
|
|
307
|
+
{
|
|
308
|
+
OutputIndex = FCString::Atoi(*OutputName);
|
|
309
|
+
}
|
|
310
|
+
else
|
|
311
|
+
{
|
|
312
|
+
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
|
|
313
|
+
for (int32 i = 0; i < Outputs.Num(); i++)
|
|
314
|
+
{
|
|
315
|
+
if (Outputs[i].OutputName.ToString().Equals(OutputName, ESearchCase::IgnoreCase))
|
|
316
|
+
{
|
|
317
|
+
OutputIndex = i;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
EMaterialProperty MatProperty;
|
|
325
|
+
if (!ParseMaterialProperty(PropertyName, MatProperty))
|
|
326
|
+
{
|
|
327
|
+
return MCPError(FString::Printf(TEXT("Unknown material property '%s'"), *PropertyName));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
Material->PreEditChange(nullptr);
|
|
331
|
+
|
|
332
|
+
UMaterialEditorOnlyData* EditorOnlyData = Material->GetEditorOnlyData();
|
|
333
|
+
if (!EditorOnlyData)
|
|
334
|
+
{
|
|
335
|
+
return MCPError(TEXT("Material has no editor-only data (is this material domain supported?)"));
|
|
336
|
+
}
|
|
337
|
+
FExpressionInput* PropertyInput = GetMaterialPropertyInput(EditorOnlyData, MatProperty);
|
|
338
|
+
if (!PropertyInput)
|
|
339
|
+
{
|
|
340
|
+
return MCPError(FString::Printf(TEXT("Material property '%s' is not supported for direct connection"), *PropertyName));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Idempotency
|
|
344
|
+
if (PropertyInput->Expression == Expression && PropertyInput->OutputIndex == OutputIndex)
|
|
345
|
+
{
|
|
346
|
+
auto Existed = MCPSuccess();
|
|
347
|
+
MCPSetExisted(Existed);
|
|
348
|
+
Existed->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
349
|
+
Existed->SetStringField(TEXT("expressionName"), ExpressionName);
|
|
350
|
+
Existed->SetStringField(TEXT("property"), PropertyName);
|
|
351
|
+
Existed->SetNumberField(TEXT("outputIndex"), OutputIndex);
|
|
352
|
+
return MCPResult(Existed);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
PropertyInput->Connect(OutputIndex, Expression);
|
|
356
|
+
|
|
357
|
+
Material->PostEditChange();
|
|
358
|
+
Material->MarkPackageDirty();
|
|
359
|
+
|
|
360
|
+
auto Result = MCPSuccess();
|
|
361
|
+
MCPSetUpdated(Result);
|
|
362
|
+
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
363
|
+
Result->SetStringField(TEXT("expressionName"), ExpressionName);
|
|
364
|
+
Result->SetStringField(TEXT("expressionClass"), Expression->GetClass()->GetName());
|
|
365
|
+
Result->SetStringField(TEXT("property"), PropertyName);
|
|
366
|
+
Result->SetNumberField(TEXT("outputIndex"), OutputIndex);
|
|
367
|
+
|
|
368
|
+
// Rollback: disconnect_material_property
|
|
369
|
+
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
370
|
+
Payload->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
371
|
+
Payload->SetStringField(TEXT("property"), PropertyName);
|
|
372
|
+
MCPSetRollback(Result, TEXT("disconnect_material_property"), Payload);
|
|
373
|
+
|
|
374
|
+
return MCPResult(Result);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
TSharedPtr<FJsonValue> FMaterialHandlers::DeleteMaterialExpression(const TSharedPtr<FJsonObject>& Params)
|
|
379
|
+
{
|
|
380
|
+
FString MaterialPath;
|
|
381
|
+
if (auto Err = RequireStringAlt(Params, TEXT("materialPath"), TEXT("path"), MaterialPath)) return Err;
|
|
382
|
+
if (MaterialPath.IsEmpty())
|
|
383
|
+
{
|
|
384
|
+
Params->TryGetStringField(TEXT("assetPath"), MaterialPath);
|
|
385
|
+
if (MaterialPath.IsEmpty())
|
|
386
|
+
{
|
|
387
|
+
return MCPError(TEXT("Missing required parameter 'materialPath' (or 'path')"));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
FString ExpressionName;
|
|
392
|
+
if (auto Err = RequireString(Params, TEXT("expressionName"), ExpressionName)) return Err;
|
|
393
|
+
|
|
394
|
+
UMaterial* Material = LoadMaterialFromPath(MaterialPath);
|
|
395
|
+
if (!Material)
|
|
396
|
+
{
|
|
397
|
+
return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *MaterialPath));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
UMaterialExpression* Expression = FindExpressionByName(Material, ExpressionName);
|
|
401
|
+
if (!Expression)
|
|
402
|
+
{
|
|
403
|
+
// Idempotent: already deleted
|
|
404
|
+
auto Noop = MCPSuccess();
|
|
405
|
+
Noop->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
406
|
+
Noop->SetStringField(TEXT("expressionName"), ExpressionName);
|
|
407
|
+
Noop->SetBoolField(TEXT("alreadyDeleted"), true);
|
|
408
|
+
return MCPResult(Noop);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
FString DeletedClass = Expression->GetClass()->GetName();
|
|
412
|
+
|
|
413
|
+
Material->PreEditChange(nullptr);
|
|
414
|
+
|
|
415
|
+
// Disconnect all references from other expressions that point to this one
|
|
416
|
+
for (UMaterialExpression* OtherExpr : Material->GetExpressions())
|
|
417
|
+
{
|
|
418
|
+
if (!OtherExpr || OtherExpr == Expression) continue;
|
|
419
|
+
for (int32 i = 0; ; i++)
|
|
420
|
+
{
|
|
421
|
+
FExpressionInput* Input = OtherExpr->GetInput(i);
|
|
422
|
+
if (!Input) break;
|
|
423
|
+
if (Input->Expression == Expression)
|
|
424
|
+
{
|
|
425
|
+
Input->Expression = nullptr;
|
|
426
|
+
Input->OutputIndex = 0;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Disconnect any material property inputs that reference this expression
|
|
432
|
+
UMaterialEditorOnlyData* EditorOnlyData = Material->GetEditorOnlyData();
|
|
433
|
+
if (EditorOnlyData)
|
|
434
|
+
{
|
|
435
|
+
auto ClearIfMatch = [Expression](FExpressionInput& Input)
|
|
436
|
+
{
|
|
437
|
+
if (Input.Expression == Expression)
|
|
438
|
+
{
|
|
439
|
+
Input.Expression = nullptr;
|
|
440
|
+
Input.OutputIndex = 0;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
ClearIfMatch(EditorOnlyData->BaseColor);
|
|
444
|
+
ClearIfMatch(EditorOnlyData->Metallic);
|
|
445
|
+
ClearIfMatch(EditorOnlyData->Specular);
|
|
446
|
+
ClearIfMatch(EditorOnlyData->Roughness);
|
|
447
|
+
ClearIfMatch(EditorOnlyData->Anisotropy);
|
|
448
|
+
ClearIfMatch(EditorOnlyData->EmissiveColor);
|
|
449
|
+
ClearIfMatch(EditorOnlyData->Opacity);
|
|
450
|
+
ClearIfMatch(EditorOnlyData->OpacityMask);
|
|
451
|
+
ClearIfMatch(EditorOnlyData->Normal);
|
|
452
|
+
ClearIfMatch(EditorOnlyData->Tangent);
|
|
453
|
+
ClearIfMatch(EditorOnlyData->WorldPositionOffset);
|
|
454
|
+
ClearIfMatch(EditorOnlyData->SubsurfaceColor);
|
|
455
|
+
ClearIfMatch(EditorOnlyData->AmbientOcclusion);
|
|
456
|
+
ClearIfMatch(EditorOnlyData->Refraction);
|
|
457
|
+
ClearIfMatch(EditorOnlyData->PixelDepthOffset);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
Material->GetExpressionCollection().RemoveExpression(Expression);
|
|
461
|
+
Material->PostEditChange();
|
|
462
|
+
Material->MarkPackageDirty();
|
|
463
|
+
|
|
464
|
+
auto Result = MCPSuccess();
|
|
465
|
+
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
466
|
+
Result->SetStringField(TEXT("deletedExpression"), ExpressionName);
|
|
467
|
+
Result->SetStringField(TEXT("deletedClass"), DeletedClass);
|
|
468
|
+
Result->SetNumberField(TEXT("expressionCount"), Material->GetExpressions().Num());
|
|
469
|
+
Result->SetBoolField(TEXT("deleted"), true);
|
|
470
|
+
// No rollback: would require snapshotting the expression and all its connections.
|
|
471
|
+
|
|
472
|
+
return MCPResult(Result);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
// disconnect_material_property -- Clear a material property input (#43)
|
|
477
|
+
// Params: materialPath, property (BaseColor, Normal, Roughness, etc.)
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// disconnect_material_property -- Clear a material property input (#43)
|
|
483
|
+
// Params: materialPath, property (BaseColor, Normal, Roughness, etc.)
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
TSharedPtr<FJsonValue> FMaterialHandlers::DisconnectMaterialProperty(const TSharedPtr<FJsonObject>& Params)
|
|
486
|
+
{
|
|
487
|
+
FString MaterialPath;
|
|
488
|
+
if (auto Err = RequireStringAlt(Params, TEXT("materialPath"), TEXT("assetPath"), MaterialPath)) return Err;
|
|
489
|
+
|
|
490
|
+
FString PropertyName;
|
|
491
|
+
if (auto Err = RequireString(Params, TEXT("property"), PropertyName)) return Err;
|
|
492
|
+
|
|
493
|
+
UMaterial* Material = LoadMaterialFromPath(MaterialPath);
|
|
494
|
+
if (!Material)
|
|
495
|
+
{
|
|
496
|
+
return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *MaterialPath));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
UMaterialEditorOnlyData* EditorOnlyData = Material->GetEditorOnlyData();
|
|
500
|
+
if (!EditorOnlyData)
|
|
501
|
+
{
|
|
502
|
+
return MCPError(TEXT("Material has no editor-only data"));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
Material->PreEditChange(nullptr);
|
|
506
|
+
|
|
507
|
+
auto ClearInput = [](FExpressionInput& Input)
|
|
508
|
+
{
|
|
509
|
+
Input.Expression = nullptr;
|
|
510
|
+
Input.OutputIndex = 0;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
FString LowerProp = PropertyName.ToLower();
|
|
514
|
+
bool bFound = true;
|
|
515
|
+
|
|
516
|
+
if (LowerProp == TEXT("basecolor")) ClearInput(EditorOnlyData->BaseColor);
|
|
517
|
+
else if (LowerProp == TEXT("metallic")) ClearInput(EditorOnlyData->Metallic);
|
|
518
|
+
else if (LowerProp == TEXT("specular")) ClearInput(EditorOnlyData->Specular);
|
|
519
|
+
else if (LowerProp == TEXT("roughness")) ClearInput(EditorOnlyData->Roughness);
|
|
520
|
+
else if (LowerProp == TEXT("anisotropy")) ClearInput(EditorOnlyData->Anisotropy);
|
|
521
|
+
else if (LowerProp == TEXT("emissivecolor") || LowerProp == TEXT("emissive")) ClearInput(EditorOnlyData->EmissiveColor);
|
|
522
|
+
else if (LowerProp == TEXT("opacity")) ClearInput(EditorOnlyData->Opacity);
|
|
523
|
+
else if (LowerProp == TEXT("opacitymask")) ClearInput(EditorOnlyData->OpacityMask);
|
|
524
|
+
else if (LowerProp == TEXT("normal")) ClearInput(EditorOnlyData->Normal);
|
|
525
|
+
else if (LowerProp == TEXT("tangent")) ClearInput(EditorOnlyData->Tangent);
|
|
526
|
+
else if (LowerProp == TEXT("worldpositionoffset")) ClearInput(EditorOnlyData->WorldPositionOffset);
|
|
527
|
+
else if (LowerProp == TEXT("subsurfacecolor")) ClearInput(EditorOnlyData->SubsurfaceColor);
|
|
528
|
+
else if (LowerProp == TEXT("ambientocclusion")) ClearInput(EditorOnlyData->AmbientOcclusion);
|
|
529
|
+
else if (LowerProp == TEXT("refraction")) ClearInput(EditorOnlyData->Refraction);
|
|
530
|
+
else if (LowerProp == TEXT("pixeldepthoffset")) ClearInput(EditorOnlyData->PixelDepthOffset);
|
|
531
|
+
else bFound = false;
|
|
532
|
+
|
|
533
|
+
if (!bFound)
|
|
534
|
+
{
|
|
535
|
+
return MCPError(FString::Printf(
|
|
536
|
+
TEXT("Unknown property '%s'. Use: BaseColor, Metallic, Specular, Roughness, EmissiveColor, Opacity, OpacityMask, Normal, Tangent, WorldPositionOffset, SubsurfaceColor, AmbientOcclusion, Refraction, PixelDepthOffset"),
|
|
537
|
+
*PropertyName));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
Material->PostEditChange();
|
|
541
|
+
Material->MarkPackageDirty();
|
|
542
|
+
|
|
543
|
+
auto Result = MCPSuccess();
|
|
544
|
+
MCPSetUpdated(Result);
|
|
545
|
+
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
|
546
|
+
Result->SetStringField(TEXT("property"), PropertyName);
|
|
547
|
+
// No rollback: we don't capture the previous expression binding before clearing.
|
|
548
|
+
|
|
549
|
+
return MCPResult(Result);
|
|
550
|
+
}
|
|
551
|
+
|