// Copyright Epic Games, Inc. All Rights Reserved. #include "BlueprintConnectionDrawingPolicy.h" #include "BlueprintEditorSettings.h" #include "Containers/Array.h" #include "Containers/EnumAsByte.h" #include "Containers/UnrealString.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphPin.h" #include "EdGraph/EdGraphSchema.h" #include "EdGraphSchema_K2.h" #include "Engine/Blueprint.h" #include "Engine/BlueprintGeneratedClass.h" #include "GraphEditorSettings.h" #include "HAL/PlatformCrt.h" #include "Internationalization/Text.h" #include "K2Node_Knot.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/KismetDebugUtilities.h" #include "Layout/ArrangedWidget.h" #include "Logging/LogCategory.h" #include "Logging/LogMacros.h" #include "Math/UnrealMathSSE.h" #include "Misc/App.h" #include "Misc/AssertionMacros.h" #include "SGraphPin.h" #include "Styling/AppStyle.h" #include "Styling/SlateBrush.h" #include "Templates/Casts.h" #include "Trace/Detail/Channel.h" #include "UObject/Class.h" #include "UObject/Object.h" #include "UObject/UObjectGlobals.h" #include "UObject/WeakObjectPtr.h" #include "UObject/WeakObjectPtrTemplates.h" class FArrangedChildren; class FSlateRect; class SWidget; ///////////////////////////////////////////////////// // FKismetConnectionDrawingPolicy FKismetConnectionDrawingPolicy::FKismetConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj) : FConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, ZoomFactor, InClippingRect, InDrawElements) , GraphObj(InGraphObj) { // Don't want to draw ending arrowheads ArrowImage = nullptr; ArrowRadius = FVector2f::ZeroVector; // But we do want to draw midpoint arrowheads if (GetDefault()->bDrawMidpointArrowsInBlueprints) { MidpointImage = FAppStyle::GetBrush( TEXT("Graph.Arrow") ); MidpointRadius = MidpointImage->ImageSize * ZoomFactor * 0.5f; } // Cache off the editor options AttackColor = Settings->TraceAttackColor; SustainColor = Settings->TraceSustainColor; ReleaseColor = Settings->TraceReleaseColor; AttackWireThickness = Settings->TraceAttackWireThickness; SustainWireThickness = Settings->TraceSustainWireThickness; ReleaseWireThickness = Settings->TraceReleaseWireThickness; DefaultDataWireThickness = Settings->DefaultDataWireThickness; DefaultExecutionWireThickness = Settings->DefaultExecutionWireThickness; TracePositionBonusPeriod = Settings->TracePositionBonusPeriod; TracePositionExponent = Settings->TracePositionExponent; AttackHoldPeriod = Settings->TraceAttackHoldPeriod; DecayPeriod = Settings->TraceDecayPeriod; DecayExponent = Settings->TraceDecayExponent; SustainHoldPeriod = Settings->TraceSustainHoldPeriod; ReleasePeriod = Settings->TraceReleasePeriod; ReleaseExponent = Settings->TraceReleaseExponent; CurrentTime = 0.0; LatestTimeDiscovered = 0.0; } void FKismetConnectionDrawingPolicy::Draw(TMap, FArrangedWidget>& InPinGeometries, FArrangedChildren& ArrangedNodes) { // Build the execution roadmap (also populates execution times) BuildExecutionRoadmap(); // Draw everything FConnectionDrawingPolicy::Draw(InPinGeometries, ArrangedNodes); } UBlueprint* FKismetConnectionDrawingPolicy::GetTargetBlueprint() const { // Start out with the current graph Blueprint context. if (UBlueprint* TargetBP = FBlueprintEditorUtils::FindBlueprintForGraph(GraphObj)) { // Macro library Blueprints have no associated "active" object debugging context, so we need to determine which one to use from the call stack. if (TargetBP->BlueprintType == BPTYPE_MacroLibrary) { // Walk backwards through the stack trace (check the most recent sample first) until we find a Blueprint context with the expansion and a valid "active" object for debugging. const TSimpleRingBuffer& TraceStack = FKismetDebugUtilities::GetTraceStack(); for (int32 i = TraceStack.Num() - 1; i >= 0 && !TargetBP->GetObjectBeingDebugged(); --i) { const FKismetTraceSample& Sample = TraceStack(i); if (UObject* TestObject = Sample.Context.Get()) { if (UBlueprintGeneratedClass* TargetClass = Cast(TestObject->GetClass())) { // Check to see if the current instruction maps back to the Macro library source graph; if it does, this Blueprint contains an expansion of the Macro source graph. const FBlueprintDebugData& DebugData = TargetClass->GetDebugData(); if (UEdGraphNode* Node = DebugData.FindSourceNodeFromCodeLocation(Sample.Function.Get(), Sample.Offset, /*bAllowImpreciseHit=*/ false)) { if (GraphObj == Node->GetGraph()) { // Switch the target Blueprint to the context containing the expansion of the current Macro source graph in the Macro library. TargetBP = CastChecked(TargetClass->ClassGeneratedBy); } } } } } } return TargetBP; } return nullptr; } bool FKismetConnectionDrawingPolicy::CanBuildRoadmap(UBlueprint* TargetBP) const { UObject* ActiveObject = GetObjectBeingDebugged(TargetBP); return ActiveObject != nullptr; } bool FKismetConnectionDrawingPolicy::CanBuildRoadmap() const { return CanBuildRoadmap(GetTargetBlueprint()); } void FKismetConnectionDrawingPolicy::BuildExecutionRoadmap() { LatestTimeDiscovered = 0.0; UBlueprint* TargetBP = GetTargetBlueprint(); // Only do highlighting in PIE or SIE if (!CanBuildRoadmap(TargetBP)) { return; } UObject* ActiveObject = GetObjectBeingDebugged(TargetBP); check(ActiveObject); // Due to CanBuildRoadmap TArray SequentialNodesInGraph; TArray SequentialNodeTimes; TArray SequentialExecPinsInGraph; { const TSimpleRingBuffer& TraceStack = FKismetDebugUtilities::GetTraceStack(); UBlueprintGeneratedClass* TargetClass = Cast(TargetBP->GeneratedClass); FBlueprintDebugData& DebugData = TargetClass->GetDebugData(); for (int32 i = 0; i < TraceStack.Num(); ++i) { const FKismetTraceSample& Sample = TraceStack(i); if (UObject* TestObject = Sample.Context.Get()) { if (TestObject == ActiveObject) { UEdGraphPin* AssociatedPin = DebugData.FindSourcePinFromCodeLocation(Sample.Function.Get(), Sample.Offset); if (UEdGraphNode* Node = DebugData.FindSourceNodeFromCodeLocation(Sample.Function.Get(), Sample.Offset, /*bAllowImpreciseHit=*/ false)) { if (GraphObj == Node->GetGraph()) { SequentialNodesInGraph.Add(Node); SequentialNodeTimes.Add(Sample.ObservationTime); SequentialExecPinsInGraph.Add(AssociatedPin); } else if (const TArray >* ExpansionSourceNodes = DebugData.FindExpansionSourceNodesFromCodeLocation(Sample.Function.Get(), Sample.Offset)) { // Attempt to find an outer expansion (tunnel instance) source node that may have also been mapped to the sample code offset. for (TWeakObjectPtr ExpansionSourceNodePtr : *ExpansionSourceNodes) { if (UEdGraphNode* TunnelInstanceNode = ExpansionSourceNodePtr.Get()) { if (GraphObj == TunnelInstanceNode->GetGraph()) { SequentialNodesInGraph.Add(TunnelInstanceNode); SequentialNodeTimes.Add(Sample.ObservationTime); SequentialExecPinsInGraph.Add(AssociatedPin); break; } } } } } } } } } // Run thru and apply bonus time const float InvNumNodes = 1.0f / (float)SequentialNodeTimes.Num(); for (int32 i = 0; i < SequentialNodesInGraph.Num(); ++i) { double& ObservationTime = SequentialNodeTimes[i]; const float PositionRatio = static_cast(SequentialNodeTimes.Num() - i) * InvNumNodes; const float PositionBonus = FMath::Pow(PositionRatio, TracePositionExponent) * TracePositionBonusPeriod; ObservationTime += PositionBonus; LatestTimeDiscovered = FMath::Max(LatestTimeDiscovered, ObservationTime); } UEdGraphPin* LastExecPin = NULL; // Record the unique exec-pin to time pairings, keeping only the most recent // times for each pairing... reverse the "SequentialNodes" because right now // it is in stack order (with the last executed node first) for (int32 i = SequentialNodesInGraph.Num() - 1; i >= 1; --i) { UEdGraphNode* CurNode = SequentialNodesInGraph[i]; UEdGraphNode* NextNode = SequentialNodesInGraph[i-1]; // keep track of the last exec-pin executed by CurNode (these tracked // pins coincide with "WireTraceSite" op-codes that have been injected // prior to every "goto" statement... this way we have context for which // pin executed the jump) if (UEdGraphPin* AssociatedPin = SequentialExecPinsInGraph[i]) { LastExecPin = AssociatedPin; } // if this statement is a jump (from one node to another) if (CurNode != NextNode) { // if there was a wire-trace op-code inserted before this jump if (LastExecPin != NULL) { //ensure(LastExecPin->GetOwningNode() == CurNode); double NextNodeTime = SequentialNodeTimes[i-1]; FExecPairingMap& ExecPaths = PredecessorPins.FindOrAdd(NextNode); FTimePair& ExecTiming = ExecPaths.FindOrAdd(LastExecPin); // make sure that if we've already visited this exec-pin (like // in a for-loop or something), that we're replacing it with a // more recent execution time // // @TODO I don't see when this wouldn't be the case if (ExecTiming.ThisExecTime < NextNodeTime) { double CurNodeTime = SequentialNodeTimes[i]; ExecTiming.ThisExecTime = NextNodeTime; ExecTiming.PredExecTime = CurNodeTime; } } // if the nodes aren't graphically connected how could they be // executed back-to-back? well, this could be a pop back to a // sequence node from the end of one thread of execution, etc. else if (AreNodesGraphicallySequential(CurNode, NextNode)) { // only warn when the nodes are directly connected (this is all // for execution flow visualization after all) UE_LOG(LogConnectionDrawingPolicy, Verbose, TEXT("Looks like a wire-trace was not injected before the jump from '%s' to '%s'."), *CurNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), *NextNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString()); } // clear the exec-pin (we're moving to a new node and want to find // it's executed out pin) LastExecPin = NULL; } // else, we're only collecting this data for tracing node-to-node // executions (so we don't care about this sequence of statements) } // Fade only when free-running (since we're using FApp::GetCurrentTime(), instead of FPlatformTime::Seconds) const double MaxTimeAhead = FMath::Min(FApp::GetCurrentTime() + 2*TracePositionBonusPeriod, LatestTimeDiscovered); //@TODO: Rough clamping; should be exposed as a parameter CurrentTime = FMath::Max(FApp::GetCurrentTime(), MaxTimeAhead); } void FKismetConnectionDrawingPolicy::CalculateEnvelopeAlphas(double ExecutionTime, /*out*/ float& AttackAlpha, /*out*/ float& SustainAlpha) const { const float DeltaTime = (float)(CurrentTime - ExecutionTime); { const float UnclampedDecayRatio = 1.0f - ((DeltaTime - AttackHoldPeriod) / DecayPeriod); const float ClampedDecayRatio = FMath::Clamp(UnclampedDecayRatio, 0.0f, 1.0f); AttackAlpha = FMath::Pow(ClampedDecayRatio, DecayExponent); } { const float SustainEndTime = AttackHoldPeriod + DecayPeriod + SustainHoldPeriod; const float UnclampedReleaseRatio = 1.0f - ((DeltaTime - SustainEndTime) / ReleasePeriod); const float ClampedReleaseRatio = FMath::Clamp(UnclampedReleaseRatio, 0.0f, 1.0f); SustainAlpha = FMath::Pow(ClampedReleaseRatio, ReleaseExponent); } } bool FKismetConnectionDrawingPolicy::TreatWireAsExecutionPin(UEdGraphPin* InputPin, UEdGraphPin* OutputPin) const { const UEdGraphSchema_K2* Schema = GetDefault(); return (InputPin != NULL) && (Schema->IsExecPin(*OutputPin)); } bool FKismetConnectionDrawingPolicy::AreNodesGraphicallySequential(UEdGraphNode* InputNode, UEdGraphNode* OutputNode) const { for (UEdGraphPin* Pin : InputNode->Pins) { if (Pin->Direction != EGPD_Output) { continue; } for (UEdGraphPin* Connection : Pin->LinkedTo) { if (!TreatWireAsExecutionPin(Pin, Connection)) { continue; } if (Connection->GetOwningNode() == OutputNode) { return true; } } } return false; } void FKismetConnectionDrawingPolicy::DetermineStyleOfExecWire(float& Thickness, FLinearColor& WireColor, bool& bDrawBubbles, const FTimePair& Times) { // It's a followed link, make it strong and yellowish but fading over time const double ExecTime = Times.ThisExecTime; float AttackAlpha; float SustainAlpha; CalculateEnvelopeAlphas(ExecTime, /*out*/ AttackAlpha, /*out*/ SustainAlpha); const float DecayedAttackThickness = FMath::Lerp(SustainWireThickness, AttackWireThickness, AttackAlpha); Thickness = FMath::Lerp(ReleaseWireThickness, DecayedAttackThickness, SustainAlpha); const FLinearColor DecayedAttackColor = FMath::Lerp(SustainColor, AttackColor, AttackAlpha); WireColor = WireColor * FMath::Lerp(ReleaseColor, DecayedAttackColor, SustainAlpha); if (SustainAlpha > KINDA_SMALL_NUMBER) { bDrawBubbles = true; } } FKismetConnectionDrawingPolicy::FTimePair const* FKismetConnectionDrawingPolicy::BackTraceExecPath(UEdGraphPin const* const OutputPin, FExecPairingMap const* const NodeExecutionList) { FTimePair const* FoundExecPath = nullptr; UEdGraphNode const* const OwningNode = OutputPin->GetOwningNode(); if (UK2Node_Knot const* const KnotNode = Cast(OwningNode)) { UEdGraphPin const* const KnotInputPin = KnotNode->GetInputPin(); for (UEdGraphPin const* KnotInput : KnotInputPin->LinkedTo) { FoundExecPath = BackTraceExecPath(KnotInput, NodeExecutionList); if (FoundExecPath != nullptr) { break; } } } else { FoundExecPath = NodeExecutionList->Find(OutputPin); } return FoundExecPath; } bool FKismetConnectionDrawingPolicy::FindPinCenter(UEdGraphPin* Pin, FVector2f& OutCenter) const { if (const TSharedPtr* pPinWidget = PinToPinWidgetMap.Find(Pin)) { if (FArrangedWidget* pPinEntry = PinGeometries->Find((*pPinWidget).ToSharedRef())) { OutCenter = FGeometryHelper::CenterOf(pPinEntry->Geometry); return true; } } return false; } UObject* FKismetConnectionDrawingPolicy::GetObjectBeingDebugged(UBlueprint* TargetBP) { UObject* ActiveObject = nullptr; if (TargetBP) { ActiveObject = TargetBP->GetObjectBeingDebugged(); if (UClass* GeneratedClass = TargetBP->GeneratedClass; ActiveObject == nullptr && BPTYPE_FunctionLibrary == TargetBP->BlueprintType && GeneratedClass) { ActiveObject = GeneratedClass->GetDefaultObject(false); } } return ActiveObject; } bool FKismetConnectionDrawingPolicy::GetAverageConnectedPosition(class UK2Node_Knot* Knot, EEdGraphPinDirection Direction, FVector2f& OutPos) const { FVector2f Result = FVector2f::ZeroVector; int32 ResultCount = 0; UEdGraphPin* Pin = (Direction == EGPD_Input) ? Knot->GetInputPin() : Knot->GetOutputPin(); for (UEdGraphPin* LinkedPin : Pin->LinkedTo) { FVector2f CenterPoint; if (FindPinCenter(LinkedPin, /*out*/ CenterPoint)) { Result += CenterPoint; ResultCount++; } } if (ResultCount > 0) { OutPos = Result * (1.0f / static_cast(ResultCount)); return true; } else { return false; } } bool FKismetConnectionDrawingPolicy::ShouldChangeTangentForKnot(UK2Node_Knot* Knot) { if (bool* pResult = KnotToReversedDirectionMap.Find(Knot)) { return *pResult; } else { bool bPinReversed = false; FVector2f AverageLeftPin; FVector2f AverageRightPin; FVector2f CenterPin; bool bCenterValid = FindPinCenter(Knot->GetOutputPin(), /*out*/ CenterPin); bool bLeftValid = GetAverageConnectedPosition(Knot, EGPD_Input, /*out*/ AverageLeftPin); bool bRightValid = GetAverageConnectedPosition(Knot, EGPD_Output, /*out*/ AverageRightPin); if (bLeftValid && bRightValid) { bPinReversed = AverageRightPin.X < AverageLeftPin.X; } else if (bCenterValid) { if (bLeftValid) { bPinReversed = CenterPin.X < AverageLeftPin.X; } else if (bRightValid) { bPinReversed = AverageRightPin.X < CenterPin.X; } } KnotToReversedDirectionMap.Add(Knot, bPinReversed); return bPinReversed; } } // Give specific editor modes a chance to highlight this connection or darken non-interesting connections void FKismetConnectionDrawingPolicy::DetermineWiringStyle(UEdGraphPin* OutputPin, UEdGraphPin* InputPin, /*inout*/ FConnectionParams& Params) { Params.WireThickness = DefaultDataWireThickness; Params.AssociatedPin1 = OutputPin; Params.AssociatedPin2 = InputPin; // Get the schema and grab the default color from it check(OutputPin); check(GraphObj); const UEdGraphSchema* Schema = GraphObj->GetSchema(); if (OutputPin->bOrphanedPin || (InputPin && InputPin->bOrphanedPin)) { Params.WireColor = FLinearColor::Red; } else { Params.WireColor = Schema->GetPinTypeColor(OutputPin->PinType); } UEdGraphNode* OutputNode = OutputPin->GetOwningNode(); UEdGraphNode* InputNode = (InputPin != nullptr) ? InputPin->GetOwningNode() : nullptr; const bool bDeemphasizeUnhoveredPins = HoveredPins.Num() > 0; // If this is a K2 graph, try to be a little more specific if (const UEdGraphSchema_K2* K2Schema = Cast(Schema)) { // If the output or input connect to a knot that is going backwards, we will flip the direction on values going into them { if (UK2Node_Knot* OutputKnotNode = Cast(OutputNode)) { if (ShouldChangeTangentForKnot(OutputKnotNode)) { Params.StartDirection = EGPD_Input; } } if (UK2Node_Knot* InputKnotNode = Cast(InputNode)) { if (ShouldChangeTangentForKnot(InputKnotNode)) { Params.EndDirection = EGPD_Output; } } } if (TreatWireAsExecutionPin(InputPin, OutputPin)) { if (CanBuildRoadmap()) { // knot nodes are removed from the graph at compile time, so we // have to follow them until we find something that would have // actually executed while (UK2Node_Knot* InputKnotNode = Cast(InputNode)) { InputNode = nullptr; UEdGraphPin* OutPin = InputKnotNode->GetOutputPin(); if (OutPin->LinkedTo.Num() > 0) { check(OutPin->LinkedTo.Num() == 1); InputNode = OutPin->LinkedTo[0]->GetOwningNode(); } } // track if this node connection was ran or not bool bExecuted = false; // if the node belonging to InputPin was actually executed if (FExecPairingMap* ExecPaths = PredecessorPins.Find(InputNode)) { // if the output pin is one of the pins that lead to InputNode being ran if (FTimePair const* ExecTiming = BackTraceExecPath(OutputPin, ExecPaths)) { bExecuted = true; DetermineStyleOfExecWire(/*inout*/ Params.WireThickness, /*inout*/ Params.WireColor, /*inout*/ Params.bDrawBubbles, *ExecTiming); } } if (!bExecuted) { // It's not followed, fade it and keep it thin Params.WireColor = ReleaseColor; Params.WireThickness = ReleaseWireThickness; } } else { // Make exec wires slightly thicker even outside of debug Params.WireThickness = DefaultExecutionWireThickness; } } else { // Container types should draw thicker if ((InputPin && InputPin->PinType.IsContainer()) || (OutputPin && OutputPin->PinType.IsContainer())) { Params.WireThickness = DefaultExecutionWireThickness; } } // If either end of the connection is not enabled (and not a passthru to something else), draw the wire differently bool bWireIsOnDisabledNodeAndNotPassthru = false; if (OutputNode && (OutputNode->IsDisplayAsDisabledForced() || !OutputNode->IsNodeEnabled())) { if (OutputNode->GetPassThroughPin(OutputPin) == nullptr) { bWireIsOnDisabledNodeAndNotPassthru = true; } } if (InputNode && (InputNode->IsDisplayAsDisabledForced() || !InputNode->IsNodeEnabled())) { if (InputNode->GetPassThroughPin(InputPin) == nullptr) { bWireIsOnDisabledNodeAndNotPassthru = true; } } if ((OutputPin && OutputPin->GetOwningNode()->IsNodeUnrelated()) || (InputPin && InputPin->GetOwningNode()->IsNodeUnrelated())) { bWireIsOnDisabledNodeAndNotPassthru = true; } if (bWireIsOnDisabledNodeAndNotPassthru) { Params.WireColor *= 0.5f; Params.WireThickness = 0.5f; } } if (bDeemphasizeUnhoveredPins) { ApplyHoverDeemphasis(OutputPin, InputPin, /*inout*/ Params.WireThickness, /*inout*/ Params.WireColor); } } void FKismetConnectionDrawingPolicy::SetIncompatiblePinDrawState(const TSharedPtr& StartPin, const TSet< TSharedRef >& VisiblePins) { if (VisiblePins.IsEmpty()) { return; } static const FLinearColor DisallowColor(0.25f, 0.25f, 0.25f, 0.5f); ResetIncompatiblePinDrawState(VisiblePins); const UEdGraphPin* StartPinObj = StartPin->GetPinObj(); const UEdGraphSchema* EdGraphSchema = StartPinObj->GetSchema(); for (auto VisiblePinIterator = VisiblePins.CreateConstIterator(); VisiblePinIterator; ++VisiblePinIterator) { TSharedPtr CheckPin = StaticCastSharedRef(*VisiblePinIterator); if (CheckPin != StartPin) { const FPinConnectionResponse Response = EdGraphSchema->CanCreateConnection(StartPinObj, CheckPin->GetPinObj()); if (Response.Response == CONNECT_RESPONSE_DISALLOW //-V1051 && !EdGraphSchema->ShouldShowPanelContextMenuForIncompatibleConnections()) { CheckPin->SetPinColorModifier(DisallowColor); } } } } void FKismetConnectionDrawingPolicy::ResetIncompatiblePinDrawState(const TSet< TSharedRef >& VisiblePins) { for (auto VisiblePinIterator = VisiblePins.CreateConstIterator(); VisiblePinIterator; ++VisiblePinIterator) { TSharedPtr VisiblePin = StaticCastSharedRef(*VisiblePinIterator); VisiblePin->SetPinColorModifier(FLinearColor::White); } }