// Copyright Epic Games, Inc. All Rights Reserved. #include "MaterialEditorHelpers.h" #include "MaterialEditor.h" #include "AssetToolsModule.h" #include "MaterialEditorUtilities.h" #include "MaterialGraph/MaterialGraphNode.h" #include "Factories/MaterialFunctionFactoryNew.h" #include "Materials/MaterialExpressionConstant.h" #include "Materials/MaterialExpressionConstant2Vector.h" #include "Materials/MaterialExpressionConstant3Vector.h" #include "Materials/MaterialExpressionConstant4Vector.h" #include "Materials/MaterialExpressionFunctionInput.h" #include "Materials/MaterialExpressionFunctionOutput.h" #include "Materials/MaterialFunction.h" #include "ScopedTransaction.h" #define LOCTEXT_NAMESPACE "MaterialEditor" void FMaterialEditorHelpers::CollapseToFunction(FMaterialEditor& MaterialEditor) { if (!ensure(MaterialEditor.OriginalMaterialObject)) { return; } TSet NodesToCollapse; for (UObject* NodeObject : MaterialEditor.GetSelectedNodes()) { UMaterialGraphNode* GraphNode = Cast(NodeObject); if (GraphNode && GraphNode->CanDuplicateNode()) { NodesToCollapse.Add(GraphNode); } } // Output pin outside the nodes to collapse -> all pins inside the nodes to collapse to connect to that new function input TMap> Inputs; // Output pin inside the nodes to collapse -> all pins outside the nodes to collapse to connect to that new function output TMap> Outputs; UEdGraph* OldGraph = nullptr; for (UEdGraphNode* Node : NodesToCollapse) { OldGraph = Node->GetGraph(); for (UEdGraphPin* Pin : Node->Pins) { for (UEdGraphPin* LinkedToPin : Pin->LinkedTo) { if (!NodesToCollapse.Contains(LinkedToPin->GetOwningNode())) { if (Pin->Direction == EGPD_Input) { Inputs.FindOrAdd(LinkedToPin).Add(Pin); } else { check(Pin->Direction == EGPD_Output); Outputs.FindOrAdd(Pin).Add(LinkedToPin); } } } } } // Expand the bounds to ensure the function input/output nodes don't overlap with the pasted nodes in the middle FBox2D NodeBounds = GetNodesBounds(MaterialEditor, NodesToCollapse); NodeBounds = NodeBounds.ExpandBy(300); // Sort inputs & outputs by the average position of the pins inside the function // This reduces the risk of having crossing links { const auto GetPinSortValue = [](UEdGraphPin* Pin) { return 100 * Pin->GetOwningNode()->NodePosY + Pin->GetOwningNode()->GetPinIndex(Pin); }; Inputs.ValueSort([&](const TArray& PinsA, const TArray& PinsB) { float PositionA = 0; for (UEdGraphPin* Pin : PinsA) { PositionA += GetPinSortValue(Pin); } PositionA /= PinsA.Num(); float PositionB = 0; for (UEdGraphPin* Pin : PinsB) { PositionB += GetPinSortValue(Pin); } PositionB /= PinsB.Num(); return PositionA < PositionB; }); Outputs.KeySort([&](UEdGraphPin& PinA, UEdGraphPin& PinB) { return GetPinSortValue(&PinA) < GetPinSortValue(&PinB); }); } if (Inputs.Num() == 0 && Outputs.Num() == 0) { // Avoids issues below return; } UMaterialFunction* NewMaterialFunction = nullptr; UMaterialGraph* NewGraph = nullptr; FMaterialEditor* NewMaterialEditor = nullptr; // Show prompt asking the user where the new asset should be placed { const FString DefaultSuffix = TEXT("_Func"); FString Name; FString PackageName; IAssetTools& AssetTools = FModuleManager::GetModuleChecked("AssetTools").Get(); AssetTools.CreateUniqueAssetName(MaterialEditor.OriginalMaterialObject->GetOutermost()->GetName(), DefaultSuffix, PackageName, Name); UMaterialFunctionFactoryNew* Factory = NewObject(); UObject* FunctionObject = AssetTools.CreateAssetWithDialog(Name, FPackageName::GetLongPackagePath(PackageName), UMaterialFunction::StaticClass(), Factory); if (!FunctionObject) { // Cancelled return; } NewMaterialFunction = Cast(FunctionObject); if (!ensure(NewMaterialFunction)) { return; } // Open the asset editor for the material function, so we can paste the nodes in it // This is somewhat hacky, but much simpler than dealing with the material expressions themselves // Needs to be done outside of the transaction, otherwise internal changes (like creating the graph schema) will be undone as well & will crash NewMaterialEditor = OpenMaterialEditorForAsset(NewMaterialFunction); if (!ensure(NewMaterialEditor) || !ensure(NewMaterialEditor->Material) || !ensure(NewMaterialEditor->Material->MaterialGraph)) { return; } NewGraph = NewMaterialEditor->Material->MaterialGraph; } // Create the new function nodes { // Make sure to not put the asset creation or OpenMaterialEditorForAsset in the transaction - that won't undo properly // We need two transactions here, as we need to call UpdateOriginalMaterial before spawning the new function call node // and calling UpdateOriginalMaterial inside a transactions seems to create some asset corruption on undo // // Example of repro when UpdateOriginalMaterial is in a transaction: // Collapse to function -> Ctrl Shift S -> Ctrl Z -> Ctrl Shift S -> the material object function is now deleted // However the asset editor for the material function is still open, and references an invalid material function. // The asset is also still visible in the content browser, but is nulled in memory (right clicking the asset will crash) const FScopedTransaction Transaction(LOCTEXT("OnCollapseToFunctionCreateNewFunction", "Collapse to function - create new function")); TMap OldGuidToNewNode; // Paste the nodes inside the new function { // Delete all default nodes // Need to take copy as DeleteNodes will remove them from the graph TArray NodesCopy = NewGraph->Nodes; NewMaterialEditor->DeleteNodes(NodesCopy, false); ensure(NewGraph->Nodes.Num() == 0); // Copy the selected nodes const FString Clipboard = MaterialEditor.CopyNodesToBuffer(NodesToCollapse); // Paste the nodes, keeping track of their new GUIDs // Note that the pins GUIDs don't change on paste - their uniqueness is only guaranteed within their node TMap OldToNewGuids; NewMaterialEditor->PasteNodesHereFromBuffer(FVector2D::ZeroVector, NewGraph, Clipboard, &OldToNewGuids); FindNewNodes(*NewMaterialEditor, OldGuidToNewNode, OldToNewGuids); } TSet UsedPinNames; const auto GetFunctionPinName = [&](UEdGraphPin* Pin) { // For function inputs/outputs, we try to give them nice names based on the pin they are linked to FName Name = Pin->PinName; if (Name.IsNone() || Name == "Output") { // Pin doesn't have a name, fall back to the node title UMaterialGraphNode* Node = Cast(Pin->GetOwningNode()); if (ensure(Node) && ensure(Node->MaterialExpression)) { TArray Captions; Node->MaterialExpression->GetCaption(Captions); if (Captions.Num() > 0) { Name = *Captions[0]; } } } // Ensure the function parameters are unique while (UsedPinNames.Contains(Name)) { Name.SetNumber(Name.GetNumber() + 1); } UsedPinNames.Add(Name); return Name; }; { int32 InputIndex = 0; for (auto& It : Inputs) { // This pin is outside of the collapsed nodes UEdGraphPin* PinLinkedToInput = It.Key; UMaterialGraphNode* GraphNodeLinkedToInput = CastChecked(PinLinkedToInput->GetOwningNode()); EMaterialValueType PinMaterialType = GraphNodeLinkedToInput->GetOutputValueType(PinLinkedToInput); EFunctionInputType PinFunctionType = {}; // This could maybe be shared with UMaterialExpressionFunctionInput, but there are tricky details involved with MCT_Float1 vs MCT_Float static_assert(FunctionInput_MAX == 13, "Need to update"); switch (PinMaterialType) { case MCT_Float1: PinFunctionType = FunctionInput_Scalar; break; case MCT_Float2: PinFunctionType = FunctionInput_Vector2; break; case MCT_Float3: PinFunctionType = FunctionInput_Vector3; break; case MCT_Float4: PinFunctionType = FunctionInput_Vector4; break; case MCT_Texture2D: PinFunctionType = FunctionInput_Texture2D; break; case MCT_TextureCube: PinFunctionType = FunctionInput_TextureCube; break; case MCT_Texture2DArray: PinFunctionType = FunctionInput_Texture2DArray; break; case MCT_VolumeTexture: PinFunctionType = FunctionInput_VolumeTexture; break; case MCT_StaticBool: PinFunctionType = FunctionInput_StaticBool; break; case MCT_MaterialAttributes: PinFunctionType = FunctionInput_MaterialAttributes; break; case MCT_TextureExternal: PinFunctionType = FunctionInput_TextureExternal; break; case MCT_Bool: PinFunctionType = FunctionInput_Bool; break; case MCT_Substrate: PinFunctionType = FunctionInput_Substrate; break; default: // Will happen pretty often with MCT_Float, as the types are rarely fully resolved // (eg Add nodes can take float1/2/3/4) PinFunctionType = FunctionInput_Scalar; } const FVector2D Location = FVector2D(-NodeBounds.GetExtent().X, (InputIndex - Inputs.Num() / 2) * 100); UMaterialExpressionFunctionInput* FunctionInput = FMaterialEditorUtilities::CreateNewMaterialExpression( NewGraph, Location, false); FunctionInput->bCollapsed = true; FunctionInput->InputName = GetFunctionPinName(PinLinkedToInput); FunctionInput->Id = FGuid::NewGuid(); FunctionInput->SortPriority = 10 * InputIndex; FunctionInput->InputType = PinFunctionType; // For each of the pins inside of the collapsed nodes, make a link to the new function input UMaterialGraphNode* FunctionInputGraphNode = CastChecked(FunctionInput->GraphNode); for (UEdGraphPin* OldPin : It.Value) { UEdGraphPin* NewPin = FindNewPin(OldPin, OldGuidToNewNode); if (!ensure(NewPin)) { continue; } FunctionInputGraphNode->GetOutputPin(0)->MakeLinkTo(NewPin); } InputIndex++; } } { int32 OutputIndex = 0; for (auto& It : Outputs) { // This pin is inside the collapsed nodes UEdGraphPin* OutputPin = It.Key; const FVector2D Location = FVector2D(NodeBounds.GetExtent().X, (OutputIndex - Outputs.Num() / 2) * 100); UMaterialExpressionFunctionOutput* FunctionOutput = FMaterialEditorUtilities::CreateNewMaterialExpression( NewGraph, Location, false); FunctionOutput->bCollapsed = true; FunctionOutput->OutputName = GetFunctionPinName(OutputPin); FunctionOutput->Id = FGuid::NewGuid(); FunctionOutput->SortPriority = 10 * OutputIndex; UMaterialGraphNode* FunctionOutputGraphNode = CastChecked(FunctionOutput->GraphNode); UMaterialGraphNode* NewGraphNode = OldGuidToNewNode.FindRef(OutputPin->GetOwningNode()->NodeGuid); if (!ensure(NewGraphNode)) { continue; } // Find OutputPin on the pasted nodes UEdGraphPin* NewPin = FindNewPin(OutputPin, OldGuidToNewNode); if (!ensure(NewPin)) { continue; } FunctionOutputGraphNode->GetInputPin(0)->MakeLinkTo(NewPin); OutputIndex++; } } } // Focus one of the newly spawned nodes - by default it'll focus the now deleted default Result output NewMaterialEditor->JumpToNode(NewGraph->Nodes[0]); // Update the material function to have the correct function pins // This MUST NOT be in a transaction NewMaterialEditor->UpdateOriginalMaterial(); const FScopedTransaction Transaction(LOCTEXT("OnCollapseToFunction", "Collapse to function")); UMaterialExpressionMaterialFunctionCall* FunctionCall = FMaterialEditorUtilities::CreateNewMaterialExpression( OldGraph, NodeBounds.GetCenter(), true); if (!ensure(FunctionCall->SetMaterialFunctionEx(nullptr, NewMaterialFunction))) { return; } UMaterialGraphNode* FunctionCallGraphNode = CastChecked(FunctionCall->GraphNode); // Connect inputs { int32 InputIndex = 0; for (auto& It : Inputs) { UEdGraphPin* PinLinkedToInput = It.Key; UEdGraphPin* InputPin = FunctionCallGraphNode->GetInputPin(InputIndex); if (ensure(InputPin)) { InputPin->MakeLinkTo(PinLinkedToInput); } InputIndex++; } } // Connect outputs { int32 OutputIndex = 0; for (auto& It : Outputs) { for (UEdGraphPin* PinLinkedToOutput : It.Value) { UEdGraphPin* OutputPin = FunctionCallGraphNode->GetOutputPin(OutputIndex); if (ensure(OutputPin)) { OutputPin->MakeLinkTo(PinLinkedToOutput); } } OutputIndex++; } } // Remove the old nodes that are now collapsed MaterialEditor.DeleteNodes(NodesToCollapse.Array(), false); // Make sure the active material editor still is the one focused MaterialEditor.FocusWindow(); } void FMaterialEditorHelpers::ExpandNode(FMaterialEditor& MaterialEditor) { TMap FunctionCalls; for (UObject* NodeObject : MaterialEditor.GetSelectedNodes()) { UMaterialGraphNode* Node = Cast(NodeObject); if (!Node) { continue; } UMaterialExpressionMaterialFunctionCall* FunctionCallExpression = Cast(Node->MaterialExpression); if (!FunctionCallExpression) { continue; } UMaterialFunctionInterface* MaterialFunction = FunctionCallExpression->MaterialFunction; if (!MaterialFunction) { continue; } FMaterialEditor* FunctionMaterialEditor = OpenMaterialEditorForAsset(MaterialFunction); if (!ensure(FunctionMaterialEditor)) { continue; } FunctionCalls.Add(Node, FunctionMaterialEditor); } // Do the transaction after all the OpenMaterialEditorForAsset are done const FScopedTransaction Transaction(LOCTEXT("ExpandNode", "Expand node")); for (auto& It : FunctionCalls) { ExpandNode(MaterialEditor, *It.Value, It.Key); } // Make the the active material editor still is the one focused MaterialEditor.FocusWindow(); } void FMaterialEditorHelpers::ExpandNode(FMaterialEditor& MaterialEditor, FMaterialEditor& FunctionMaterialEditor, UMaterialGraphNode* FunctionCallNode) { if (!ensure(MaterialEditor.Material) || !ensure(MaterialEditor.Material->MaterialGraph)) { return; } UMaterialGraph* Graph = MaterialEditor.Material->MaterialGraph; UMaterialExpressionMaterialFunctionCall* FunctionCallExpression = CastChecked(FunctionCallNode->MaterialExpression); // Copy the function nodes into this graph TMap OldGuidToNewNode; TMap IdsToFunctionInputs; TMap IdsToFunctionOutputs; FVector2D PastePositionOffset(ForceInit); { if (!ensure(FunctionMaterialEditor.OriginalMaterial) || !ensure(FunctionMaterialEditor.OriginalMaterial->MaterialGraph)) { return; } TSet NodesToCopy(FunctionMaterialEditor.OriginalMaterial->MaterialGraph->Nodes); // Make sure to not copy the function input/outputs - these can't be copied into materials for (auto It = NodesToCopy.CreateIterator(); It; ++It) { UMaterialGraphNode* MaterialNode = Cast(*It); if (!MaterialNode) { continue; } UMaterialExpressionFunctionInput* FunctionInput = Cast(MaterialNode->MaterialExpression); if (FunctionInput) { ensure(!IdsToFunctionInputs.Contains(FunctionInput->Id)); IdsToFunctionInputs.Add(FunctionInput->Id, FunctionInput); It.RemoveCurrent(); continue; } UMaterialExpressionFunctionOutput* FunctionOutput = Cast(MaterialNode->MaterialExpression); if (FunctionOutput) { ensure(!IdsToFunctionOutputs.Contains(FunctionOutput->Id)); IdsToFunctionOutputs.Add(FunctionOutput->Id, FunctionOutput); It.RemoveCurrent(); continue; } } const FString Clipboard = FunctionMaterialEditor.CopyNodesToBuffer(NodesToCopy); const FVector2D PasteLocation(FunctionCallNode->NodePosX, FunctionCallNode->NodePosY); // Compute the offset in a similar way PasteNodesHereFromBuffer does it, if we need to add new node for input previews FVector2D AveragePosition = FVector2D::ZeroVector; for (UEdGraphNode* Node : NodesToCopy) { AveragePosition.X += Node->NodePosX; AveragePosition.Y += Node->NodePosY; } AveragePosition /= NodesToCopy.Num(); PastePositionOffset = PasteLocation - AveragePosition; TMap OldToNewGuids; MaterialEditor.PasteNodesHereFromBuffer(PasteLocation, Graph, Clipboard, &OldToNewGuids); FindNewNodes(MaterialEditor, OldGuidToNewNode, OldToNewGuids); } TArray FunctionCallInputPins; TArray FunctionCallOutputPins; TArray Pins = FunctionCallNode->Pins; for (int32 PinIndex = 0; PinIndex < Pins.Num(); PinIndex++) { if (Pins[PinIndex]->Direction == EGPD_Input) { FunctionCallInputPins.Add(Pins[PinIndex]); } else { FunctionCallOutputPins.Add(Pins[PinIndex]); } } if (!ensure(FunctionCallInputPins.Num() == FunctionCallExpression->FunctionInputs.Num()) || !ensure(FunctionCallOutputPins.Num() == FunctionCallExpression->FunctionOutputs.Num())) { return; } // Fixup inputs, making sure to handle preview values correctly for (int32 InputIndex = 0; InputIndex < FunctionCallInputPins.Num(); InputIndex++) { UEdGraphPin* FunctionCallInputPin = FunctionCallInputPins[InputIndex]; const FFunctionExpressionInput& FunctionCallInput = FunctionCallExpression->FunctionInputs[InputIndex]; UMaterialExpressionFunctionInput* FunctionInput = IdsToFunctionInputs.FindRef(FunctionCallInput.ExpressionInputId); UMaterialGraphNode* FunctionInputNode = FunctionInput ? Cast(FunctionInput->GraphNode) : nullptr; if (!ensure(FunctionInput) || !ensure(FunctionInputNode)) { continue; } UEdGraphPin* PinLinkedToInput = nullptr; if (FunctionCallInputPin->LinkedTo.Num() > 0) { // If the input is connected, just use that as input ensure(FunctionCallInputPin->LinkedTo.Num() == 1); PinLinkedToInput = FunctionCallInputPin->LinkedTo[0]; } else { // If no input connected, we need to figure out what to do with preview pins // if bUsePreviewValueAsDefault is false this is technically a compilation error - but it's better if we just ignore that and use the preview value anyway TArray InputPins; for (int32 PinIndex = 0; PinIndex < FunctionInputNode->Pins.Num(); PinIndex++) { if (FunctionInputNode->Pins[PinIndex]->Direction == EGPD_Input) { InputPins.Add(FunctionInputNode->Pins[PinIndex]); } } if (!ensure(InputPins.Num() == 1)) { continue; } if (InputPins[0]->LinkedTo.Num() > 0) { ensure(InputPins[0]->LinkedTo.Num() == 1); PinLinkedToInput = FindNewPin(InputPins[0]->LinkedTo[0], OldGuidToNewNode); } else { // See UMaterialExpressionFunctionInput::CompilePreviewValue const FVector2D NewNodePosition = FVector2D(FunctionInputNode->NodePosX, FunctionInputNode->NodePosY) + PastePositionOffset; switch (FunctionInput->InputType) { case FunctionInput_Scalar: { UMaterialExpressionConstant* Constant = FMaterialEditorUtilities::CreateNewMaterialExpression( Graph, NewNodePosition, false); Constant->R = FunctionInput->PreviewValue.X; PinLinkedToInput = CastChecked(Constant->GraphNode)->GetOutputPin(0); break; } case FunctionInput_Vector2: { UMaterialExpressionConstant2Vector* Constant = FMaterialEditorUtilities::CreateNewMaterialExpression( Graph, NewNodePosition, false); Constant->R = FunctionInput->PreviewValue.X; Constant->G = FunctionInput->PreviewValue.Y; PinLinkedToInput = CastChecked(Constant->GraphNode)->GetOutputPin(0); break; } case FunctionInput_Vector3: { UMaterialExpressionConstant3Vector* Constant = FMaterialEditorUtilities::CreateNewMaterialExpression( Graph, NewNodePosition, false); Constant->Constant = FLinearColor(FunctionInput->PreviewValue); PinLinkedToInput = CastChecked(Constant->GraphNode)->GetOutputPin(0); break; } case FunctionInput_Vector4: { UMaterialExpressionConstant4Vector* Constant = FMaterialEditorUtilities::CreateNewMaterialExpression( Graph, NewNodePosition, false); Constant->Constant = FLinearColor(FunctionInput->PreviewValue); PinLinkedToInput = CastChecked(Constant->GraphNode)->GetOutputPin(0); break; } default: break; } } } if (!PinLinkedToInput) { // Can happen if the preview value is not supported continue; } TArray OutputPins; for (int32 PinIndex = 0; PinIndex < FunctionInputNode->Pins.Num(); PinIndex++) { if (FunctionInputNode->Pins[PinIndex]->Direction == EGPD_Output) { OutputPins.Add(FunctionInputNode->Pins[PinIndex]); } } if (!ensure(OutputPins.Num() == 1)) { continue; } for (UEdGraphPin* LinkedToInMaterialFunction : OutputPins[0]->LinkedTo) { UEdGraphPin* LinkedToInPastedNodes = FindNewPin(LinkedToInMaterialFunction, OldGuidToNewNode); if (ensure(LinkedToInPastedNodes)) { PinLinkedToInput->MakeLinkTo(LinkedToInPastedNodes); } } } // Fixup outputs for (int32 OutputIndex = 0; OutputIndex < FunctionCallOutputPins.Num(); OutputIndex++) { UEdGraphPin* FunctionCallOutputPin = FunctionCallOutputPins[OutputIndex]; const FFunctionExpressionOutput& FunctionCallOutput = FunctionCallExpression->FunctionOutputs[OutputIndex]; UMaterialExpressionFunctionOutput* FunctionOutput = IdsToFunctionOutputs.FindRef(FunctionCallOutput.ExpressionOutputId); UMaterialGraphNode* FunctionOutputNode = FunctionOutput ? Cast(FunctionOutput->GraphNode) : nullptr; if (!ensure(FunctionOutput) || !ensure(FunctionOutputNode)) { continue; } TArray InputPins; for (int32 PinIndex = 0; PinIndex < FunctionOutputNode->Pins.Num(); PinIndex++) { if (FunctionOutputNode->Pins[PinIndex]->Direction == EGPD_Input) { InputPins.Add(FunctionOutputNode->Pins[PinIndex]); } } if (!ensure(InputPins.Num() == 1)) { continue; } if (InputPins[0]->LinkedTo.Num() == 0) { // Function output is not connected continue; } ensure(InputPins[0]->LinkedTo.Num() == 1); UEdGraphPin* LinkedToInPastedNodes = FindNewPin(InputPins[0]->LinkedTo[0], OldGuidToNewNode); if (!ensure(LinkedToInPastedNodes)) { continue; } for (UEdGraphPin* PinLinkedToOutput : FunctionCallOutputPin->LinkedTo) { PinLinkedToOutput->MakeLinkTo(LinkedToInPastedNodes); } } // Finally, delete the function call node MaterialEditor.DeleteNodes({ FunctionCallNode }, false); } FBox2D FMaterialEditorHelpers::GetNodesBounds(FMaterialEditor& MaterialEditor, const TSet& Nodes) { FBox2D Bounds(ForceInit); for (UEdGraphNode* Node : Nodes) { FSlateRect Rect; MaterialEditor.GetBoundsForNode(Node, Rect, 0); Bounds += Rect.GetBottomLeft(); Bounds += Rect.GetTopRight(); } return Bounds; } void FMaterialEditorHelpers::FindNewNodes(FMaterialEditor& MaterialEditor, TMap& OutOldGuidToNewNode, const TMap& OldToNewGuids) { if (!ensure(MaterialEditor.Material) || !ensure(MaterialEditor.Material->MaterialGraph)) { return; } UMaterialGraph* Graph = MaterialEditor.Material->MaterialGraph; TMap GuidToNode; for (UEdGraphNode* Node : Graph->Nodes) { GuidToNode.Add(Node->NodeGuid, Cast(Node)); } for (auto& It : OldToNewGuids) { OutOldGuidToNewNode.Add(It.Key, GuidToNode.FindRef(It.Value)); } } UEdGraphPin* FMaterialEditorHelpers::FindNewPin(UEdGraphPin* OldPin, const TMap& OldGuidToNewNode) { if (!ensure(OldPin) || !ensure(OldPin->GetOwningNode())) { return nullptr; } UMaterialGraphNode* NewGraphNode = OldGuidToNewNode.FindRef(OldPin->GetOwningNode()->NodeGuid); if (!ensure(NewGraphNode)) { return nullptr; } // Find OldPin on the pasted nodes // Pin GUIDs aren't changed on paste UEdGraphPin* NewPin = nullptr; for (UEdGraphPin* Pin : NewGraphNode->Pins) { if (Pin->PinId == OldPin->PinId) { NewPin = Pin; } } ensure(NewPin); return NewPin; } FMaterialEditor* FMaterialEditorHelpers::OpenMaterialEditorForAsset(UObject* Asset) { UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); AssetEditorSubsystem->OpenEditorForAsset(Asset); IAssetEditorInstance* AssetEditor = AssetEditorSubsystem->FindEditorForAsset(Asset, true); if (!ensure(AssetEditor) || // Clumsy type safety check just in case - see FMaterialEditor::GetToolkitFName !ensure(AssetEditor->GetEditorName() == "MaterialEditor")) { return nullptr; } return static_cast(AssetEditor); } #undef LOCTEXT_NAMESPACE