Files
UnrealEngineUWP/Engine/Plugins/Runtime/Metasound/Source/MetasoundFrontend/Public/MetasoundFrontendBaseClasses.h
Ethan Geller acb32a298b #jira UEAU-467
Initial implementation of Metasound::Frontend. Includes all classes and abstractions for dealing with the metasound document tree and graph editing.

Overall super happy with the way this turned out, since it lets us support exporting metasound graphs, creating local subgraphs in a metasound graph, and other really cool things.

High Level:
1) a Metasound with any sorts of inputs and outputs can be created using a UMetasoundNodeAsset, which contains a struct called the FMetasoundDocument.
2) since it would be very easy to create a malformed FMetasoundDocument by just editing the struct directly, all editing of the FMetasoundDocument is done through a set of APIs: FGraphHandle, FNodeHandle, FInputHandle, and FOutputHandle
2) You can access the main graph for a UMetasoundNodeAsset with UMetasoundNodeAsset::GetRootGraphHandle(), which returns a Metasound::Frontend::FGraphHandle.
3) The FGraphHandle lets you do all sorts of things, including:
   i) Adding and removing nodes from the graph (AddNewNode/RemoveNode)
   ii) adding and removing inputs/outputs from a graph (AddNewInput/RemoveInput, AddNewOutput/RemoveOutput)
   iii) Get a handle to manipulate individual nodes in the graph (GetAllNodes/GetOutputNodes/GetInputNodes/GetOutputNodeWithName/GetInputNodeWithName)
   iv) Get any metadata associated with that graph (GetGraphMetadata)
   v) Create a subgraph that lives in the same document (CreateEmptySubgraphNode)
   vi) Export a graph to a new Metasound asset or JSON
   vii) compile that graph to an executable, playable, Metasound::IOperator (currently not all the way implemented in this CL but I'm getting to that tomorrow)
4) The FNodeHandle lets you manipulate any indivdual node on any given graph, primarily:
    i) Getting input pins (GetAllInputs/GetAllOutputs/GetInputWithName/GetOutputWithName)
    ii) get an FGraphHandle for the subgraph for this node if there is one (GetContainedGraph)
5) FInputHandle/FOutputHandle lets you manipulate individual input/output pins, primarily:
   i) Seeing if two pins can be connected (CanConnectTo)
   ii) Connecting two pins (Connect/ConnectWithConverterNode, the latter of which isn't implemented yet)

Mid Level (the metasound document model):
1) FMetasoundDocument is an aggregate and totally serializable to binary or text using the uproperty serializer.
2) A FMetasoundDocument owns a root class, which is it's primary graph, and a list of dependencies, which could be c++ implemented nodes, metasound graphs living in other assets, or subgraphs living in the current metasound document. the Dependencies list owns a list of any dependencies for the root class, as well as any nested dependencies (for example, if the root class has a subgraph that requires a C++ implemented sine osc, the subgraph and the sine osc will both be in the Dependencies array.
3) Each FMetasoundClassDescription contains that metasounds metadata, as well as a list of inputs and outputs for that metasound, and optionally a fully implemented graph.
4) If the metasound class description has a fully implemented graph, that graph is a list of FMetasoundNodeDescriptions, which themselves describe which nodes they are connected to.
5) Each FMetasoundClassDescription contains a list of uint32s corresponding to a dependency in the document's Dependency list.

Low level (how individual elements of the FMetasoundDocument are accessed and edited)
1) We can't hand out weak pointers to the FMetasoundDocument itself unfortunately, but We are able to enforce a strict visitor pattern using the class FMetasound::Frontend:: FDescriptionAccessPoint. Currently, uobjects like UMetasoundNodeAsset can hand out weak pointers to the FDescriptionAccessPoint, and the FDescriptionAccessPoint can return raw pointers to specific elements in the FMetasoundDocument using FDescriptionAccessPoint::GetElementAtPath. Since individual elements in the document can get moved around for a number of reasons (for example, we added or removed a dependency to the document, or a node to a specific subgraph), we enforce that specific elements get accessed via a path that doesn't cache specific indices or references to specific nested structs.
2) Paths to specific elements are saved as FDescPaths, which is basically an array of enums/uint32s/strings. I added some syntactical sugar using bracket overloads to more easily build out paths to specific elements. At first this looks whacky, but after a few uses it ends up being pretty easy to write paths using chained bracket operators. All paths start at a document type, and autocomplete does most of the hard work using a set of enums.

For example, to access a specific node on a specific subgraph (which is the most complicated use case):
FDescPath()[Path::EFromDocument::ToDepenencies][TEXT("MyCoolSubgraph")][Path::EFromClass::ToGraph][Path::EFromGraph::ToNodes][12 /* or whatever node id I want */];
3) Finally, Metasound::Frontend:::TDescriptionPtr wraps a weak ptr to a FDescriptionAccessPoint and a FDescPath and ends up getting used like a weak pointer. Under the hood though, it's enforcing a visitor pattern.
4) All of the big branching statements for following a FDescPath to a specific element in the metasound document is hidden in FDescriptionAccessPoint::GoToNext, which uses TVariants to cache the current step and go to the next step.
5) Most of the complicated parts of implementing FGraphHandle is dynamically adding and removing dependencies to documents and classes behind the scenes when we add/remove new nodes.
6) I also added support for basic undo/redo callbacks, but haven't explicitly added em yet to any specific handles.

#rb rob.gay

[CL 13829763 by Ethan Geller in ue5-main branch]
2020-07-02 23:05:41 -04:00

659 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Misc/TVariant.h"
#include "MetasoundFrontendDataLayout.h"
namespace Metasound
{
namespace Frontend
{
// Utility base class for classes that want to support naive multi-level undo/redo.
class ITransactable : public TSharedFromThis<ITransactable>
{
public:
ITransactable() = delete;
ITransactable(uint32 InUndoLimit, TWeakObjectPtr<UObject> InOwningAsset = nullptr);
/*
* Undo a single operation for this or any of it's owned transactables.
* @returns true on success.
*/
bool Undo();
/*
* Redo a single operation for this or any of it's owned transactables.
* @returns true on success.
*/
bool Redo();
protected:
struct FReversibleTransaction
{
// Function that will be executed when Undo is called.
// This should return true on success.
TFunction<bool()> UndoTransaction;
// Function that will be executed when Redo is called.
// This should return true on success.
TFunction<bool()> RedoTransaction;
FReversibleTransaction(TFunction<bool()>&& InUndo, TFunction<bool()>&& InRedo)
: UndoTransaction(MoveTemp(InUndo))
, RedoTransaction(MoveTemp(InRedo))
{
}
};
// Commits an undoable/redoable transaction for this object.
void CommitTransaction(FReversibleTransaction&& InTransactionDescription);
// Use this function to denote an ITransactable that should consider this ITransactable part of it's undo roll.
// For example, if an undoable transaction is committed to a FNodeHandle,
// and you want FGraphHandle::Undo to be able to undo that transaction,
// you would call FInputHandle::RegisterOwningTransactable(OwningGraphHandle).
//
// @returns true on success, false if registering this as an owning transaction would create a cycle.
bool RegisterOwningTransactable(ITransactable& InOwningTransactable);
private:
// Execute the topmost transaction on the undo stack,
// push it to redo stack,
// and notify the OnwingTransactable.
bool PerformLocalUndo();
// Execute the topmost transaction on the redo stack,
// push it to the undo stack,
// and notify the OwningTransactable.
bool PerformLocalRedo();
// Called from a owned transactable to denote that it is popping an undo or redo action.
// When this occurs, we discard the topmost call to the owned transactable from the UndoTransactableStack.
bool DiscardUndoFromOwnedTransactable(TWeakPtr<ITransactable> InOwnedTransactable);
bool DiscardRedoFromOwnedTransactable(TWeakPtr<ITransactable> InOwnedTransactable);
bool PushUndoFromOwnedTransactable(TWeakPtr<ITransactable> InOwnedTransactable);
bool PushRedoFromOwnedTransactable(TWeakPtr<ITransactable> InOwnedTransactable);
// TODO: Make these ring buffers to avoid constantly moving elements around.
TArray<TWeakPtr<ITransactable>> UndoTransactableStack;
TArray<TWeakPtr<ITransactable>> RedoTransactableStack;
TArray<FReversibleTransaction> LocalUndoTransactionStack;
TArray<FReversibleTransaction> LocalRedoTransactionStack;
// When CommitTransaction is called,
// this ITransactable is pushed to the ITransactable::TransactableStack of this ITransactable object.
TWeakPtr<ITransactable> OwningTransactable;
protected:
uint32 UndoLimit;
// If this transactable is operating on a UAsset, this can be filled to mark it's corresponding package as dirty.
TWeakObjectPtr<UObject> OwningAsset;
};
namespace Path
{
// This series of enums is used to denote
// the path to a specific element in the class description.
// This enum is every possible Description struct we have.
enum class EDescType : uint8
{
Document, // FMetasoundDocument
Class, // FMetasoundClassDescription
Metadata, // FMetasoundClassMetadata
DocDependencies, // TArray<FMetasoundClassDescription>
ClassDependencies,
Graph, //
Nodes,
Node,
Inputs,
Input,
Outputs,
Output,
Invalid
};
// This series of enums is used to delineate which description types are direct children or other descripiton types.
enum class EFromDocument : uint32
{
ToRootClass,
ToDependencies
};
enum class EFromClass : uint32
{
ToMetadata,
ToInputs,
ToOutputs,
ToDependencies,
ToGraph,
ToInvalid
};
enum class EFromGraph : uint32
{
ToNodes,
Invalid
};
struct FElement
{
EDescType CurrentDescType;
// This is only used if EDescType is Inputs, Outputs, or Dependencies.
FString LookupName;
// This is only used if EDescType == Node.
uint32 LookupID;
FElement()
: CurrentDescType(EDescType::Invalid)
, LookupID(INDEX_NONE)
{}
};
}
// This struct represents a selector for a specific element in a FMetasoundClassDescription.
// Typically these will be used with FDescriptionAccessPoint::GetElementAtPath.
struct FDescPath
{
TArray<Path::FElement> Path;
FDescPath()
{
Path::FElement RootElement;
RootElement.CurrentDescType = Path::EDescType::Document;
Path.Add(RootElement);
}
int32 Num() const
{
return Path.Num();
}
bool IsValid() const
{
return Path.Num() > 0 &&
Path[0].CurrentDescType == Path::EDescType::Document;
}
/**
* Bracket operator used for setting up a path into a metasound class description.
* These can be chained, for example:
* FDescPath PathToNode = RootClass[Path::EFromClass::ToGraph][Path::EFromGraph::ToNodes][12];
*/
FDescPath operator[](Path::EFromDocument InElement) const
{
if (!ensureAlwaysMsgf(Path.Num() > 0, TEXT("Tried to append to a path that had no root.")))
{
return FDescPath();
}
// Copy the old path, and based on the last element in that path, append a new child element.
FDescPath NewPath = *this;
const Path::FElement& LastElementInPath = Path.Last();
if(!ensureAlwaysMsgf(LastElementInPath.CurrentDescType == Path::EDescType::Document, TEXT("Tried to build an invalid path.")))
{
return FDescPath();
}
Path::FElement NewElement = Path::FElement();
switch (InElement)
{
case Path::EFromDocument::ToRootClass:
{
NewElement.CurrentDescType = Path::EDescType::Class;
break;
}
case Path::EFromDocument::ToDependencies:
{
NewElement.CurrentDescType = Path::EDescType::DocDependencies;
break;
}
}
NewPath.Path.Add(NewElement);
return NewPath;
}
FDescPath operator[](Path::EFromClass InElement) const
{
if (!ensureAlwaysMsgf(Path.Num() > 0, TEXT("Tried to append to a path that had no root.")))
{
return FDescPath();
}
// Copy the old path, and based on the last element in that path, append a new child element.
FDescPath NewPath = *this;
const Path::FElement& LastElementInPath = Path.Last();
if (!ensureAlwaysMsgf(LastElementInPath.CurrentDescType == Path::EDescType::Class, TEXT("Tried to build an invalid path.")))
{
return FDescPath();
}
Path::FElement NewElement = Path::FElement();
switch (InElement)
{
case Path::EFromClass::ToDependencies:
{
NewElement.CurrentDescType = Path::EDescType::ClassDependencies;
break;
}
case Path::EFromClass::ToGraph:
{
NewElement.CurrentDescType = Path::EDescType::Graph;
break;
}
case Path::EFromClass::ToInputs:
{
NewElement.CurrentDescType = Path::EDescType::Inputs;
break;
}
case Path::EFromClass::ToOutputs:
{
NewElement.CurrentDescType = Path::EDescType::Outputs;
break;
}
case Path::EFromClass::ToInvalid:
default:
{
ensureAlwaysMsgf(false, TEXT("Invalid path set up. Returning an empty path."));
return FDescPath();
break;
}
}
NewPath.Path.Add(NewElement);
return NewPath;
}
FDescPath operator[](Path::EFromGraph InElement) const
{
if (!ensureAlwaysMsgf(Path.Num() > 0, TEXT("Tried to append to a path that had no root.")))
{
return FDescPath();
}
// Copy the old path, and based on the last element in that path, append a new child element.
FDescPath NewPath = *this;
const Path::FElement& LastElementInPath = Path.Last();
if (!ensureAlwaysMsgf(LastElementInPath.CurrentDescType == Path::EDescType::Graph, TEXT("Tried to build an invalid path.")))
{
return FDescPath();
}
Path::FElement NewElement = Path::FElement();
switch (InElement)
{
case Path::EFromGraph::ToNodes:
{
NewElement.CurrentDescType = Path::EDescType::Nodes;
break;
}
case Path::EFromGraph::Invalid:
default:
{
ensureAlwaysMsgf(false, TEXT("Invalid path set up. Returning an empty path."));
return FDescPath();
break;
}
}
NewPath.Path.Add(NewElement);
return NewPath;
}
FDescPath operator[](uint32 InElement) const
{
if (!ensureAlwaysMsgf(Path.Num() > 0, TEXT("Tried to append to a path that had no root.")))
{
return FDescPath();
}
// Copy the old path, and based on the last element in that path, append a new child element.
FDescPath NewPath = *this;
const Path::FElement& LastElementInPath = Path.Last();
if (!ensureAlwaysMsgf(LastElementInPath.CurrentDescType == Path::EDescType::Nodes || LastElementInPath.CurrentDescType == Path::EDescType::DocDependencies, TEXT("Tried to build an invalid path.")))
{
return FDescPath();
}
Path::FElement NewElement = Path::FElement();
if (LastElementInPath.CurrentDescType == Path::EDescType::Nodes)
{
NewElement.CurrentDescType = Path::EDescType::Node;
NewElement.LookupID = InElement;
}
else if (LastElementInPath.CurrentDescType == Path::EDescType::ClassDependencies)
{
NewElement.CurrentDescType = Path::EDescType::DocDependencies;
NewElement.LookupID = InElement;
}
NewPath.Path.Add(NewElement);
return NewPath;
}
FDescPath operator [](const TCHAR* InElementName) const
{
if (!ensureMsgf(Path.Num() > 0, TEXT("Tried to append to a path that had no root.")))
{
return FDescPath();
}
// Copy the old path, and based on the last element in that path, append a new child element.
FDescPath NewPath = *this;
const Path::FElement& LastElementInPath = Path.Last();
Path::FElement NewElement = Path::FElement();
switch (LastElementInPath.CurrentDescType)
{
case Path::EDescType::DocDependencies:
{
NewElement.CurrentDescType = Path::EDescType::Class;
NewElement.LookupName = FString(InElementName);
break;
}
case Path::EDescType::Inputs:
{
NewElement.CurrentDescType = Path::EDescType::Input;
NewElement.LookupName = FString(InElementName);
break;
}
case Path::EDescType::Outputs:
{
NewElement.CurrentDescType = Path::EDescType::Output;
NewElement.LookupName = FString(InElementName);
break;
}
/** Class descriptions and graph descriptions are navigated via enums. */
case Path::EDescType::Class:
case Path::EDescType::Graph:
{
ensureAlwaysMsgf(false, TEXT("Invalid path set up. Classes and Graphs are accessed by using the DescriptionPath::EClass and DesciptionPath::EGraph enums, respectively."));
return FDescPath();
break;
}
/** Dependencies for individual classes are navigated to via a uint32. */
case Path::EDescType::ClassDependencies:
{
ensureAlwaysMsgf(false, TEXT("Invalid path set up. Dependencies for individual classes are accessed by using a uint32."));
return FDescPath();
break;;
}
/** Node list descriptions are navigated via a uint32. */
case Path::EDescType::Nodes:
{
ensureAlwaysMsgf(false, TEXT("Invalid path set up. Node lists are accessed by using a uint32."));
return FDescPath();
break;
}
/** Individiual inputs, outpus and nodes, as well as metadata, do not have any child elements. */
case Path::EDescType::Input:
case Path::EDescType::Output:
case Path::EDescType::Node:
case Path::EDescType::Metadata:
default:
{
ensureAlwaysMsgf(false, TEXT("Invalid path. Tried to add more pathing off of a description type that has no children."));
return FDescPath();
break;
}
}
NewPath.Path.Add(NewElement);
return NewPath;
}
// This can be used to go up multiple levels in a path. Effectively removes the last UnwindCount elements from the path.
FDescPath& operator<< (uint32 UnwindCount)
{
if (!ensureAlwaysMsgf(((uint32)Path.Num()) > UnwindCount, TEXT("Tried to unwind past the root of an FDescPath!")))
{
return *this;
}
while (UnwindCount-- > 0)
{
Path.RemoveAt(Path.Num() - 1);
}
return *this;
}
};
// Series of utility functions for description paths.
namespace Path
{
// When given a path for a node, finds that node's declaration in the Dependencies list.
FDescPath GetPathToClassForNode(FDescPath InPathForNode, FString& InNodeName);
// Returns a path for whatever class description encapsulates the given graph.
FDescPath GetOwningClassDescription(FDescPath InPathForGraph);
FDescPath GetClassDescription(const FString& InClassName);
FDescPath GetDependencyPath(const FString& InDependencyName);
FDescPath GetDependencyPath(uint32 InDependencyID);
// Generates a human readable string for a path into a Metasound description. Used for debugging.
FString GetPrintableString(FDescPath InPath);
}
typedef TArray<uint32> FClassDependencyIDs;
/**
* Utility class that lives alongside a FMetasoundClassDescription to allow it's individual elements to be safely read and edited.
* Note: Could be moved to a private header.
* It's also only necessary because we can't wrap a FMetasoundClassDescription in a shared ptr and also use it as a UPROPERTY.
*/
class FDescriptionAccessPoint
{
public:
FDescriptionAccessPoint() = delete;
FDescriptionAccessPoint(FMetasoundDocument& InRootDocument);
// Returns the root class description associated with this FDescriptionAccessPoint.
FMetasoundDocument& GetRoot();
// Get a descriptor element at a specific path from the root of a metasound.
// @see FDescPath for more details.
// @returns a pointer to a descriptor struct, or an invalid ptr if the path is invalid.
template <typename MetasoundDescriptionType>
MetasoundDescriptionType* GetElementAtPath(const FDescPath& InPathFromRoot)
{
if (TIsSame<MetasoundDescriptionType, FMetasoundClassDescription>::Value)
{
return (MetasoundDescriptionType*) GetClassFromPath(InPathFromRoot);
}
else if (TIsSame<MetasoundDescriptionType, FMetasoundNodeDescription>::Value)
{
return (MetasoundDescriptionType*) GetNodeFromPath(InPathFromRoot);
}
else if (TIsSame<MetasoundDescriptionType, FMetasoundGraphDescription>::Value)
{
return (MetasoundDescriptionType*) GetGraphFromPath(InPathFromRoot);
}
else if (TIsSame<MetasoundDescriptionType, FMetasoundInputDescription>::Value)
{
return (MetasoundDescriptionType*) GetInputFromPath(InPathFromRoot);
}
else if (TIsSame<MetasoundDescriptionType, FMetasoundOutputDescription>::Value)
{
return (MetasoundDescriptionType*) GetOutputFromPath(InPathFromRoot);
}
else if (TIsSame<MetasoundDescriptionType, FMetasoundClassMetadata>::Value)
{
return (MetasoundDescriptionType*) GetMetadataFromPath(InPathFromRoot);
}
else if (TIsSame<MetasoundDescriptionType, FClassDependencyIDs>::Value)
{
return (MetasoundDescriptionType*) GetClassDependencyIDsFromPath(InPathFromRoot);
}
else
{
ensureAlwaysMsgf(false, TEXT("Tried to call GetElementAtPath with an invalid type."));
return nullptr;
}
}
private:
// The root class description for all description structs
// accessed via this class.
FMetasoundDocument& RootDocument;
// These functions result in some duplicate code, but allow us to hide
// the implementation for GetElementAtPath.
FMetasoundClassDescription* GetClassFromPath(const FDescPath& InPathFromRoot);
FMetasoundNodeDescription* GetNodeFromPath(const FDescPath& InPathFromRoot);
FMetasoundGraphDescription* GetGraphFromPath(const FDescPath& InPathFromRoot);
FMetasoundInputDescription* GetInputFromPath(const FDescPath& InPathFromRoot);
FMetasoundOutputDescription* GetOutputFromPath(const FDescPath& InPathFromRoot);
FMetasoundClassMetadata* GetMetadataFromPath(const FDescPath& InPathFromRoot);
FClassDependencyIDs* GetClassDependencyIDsFromPath(const FDescPath& InPathFromRoot);
using FMetasoundDescriptionPtr = TVariant<
FMetasoundDocument*,
FMetasoundClassDescription*,
FMetasoundNodeDescription*,
FMetasoundGraphDescription*,
FMetasoundInputDescription*,
FMetasoundOutputDescription*,
FMetasoundClassMetadata*,
FClassDependencyIDs*,
void*>
;
struct FDescriptionUnwindStep
{
FMetasoundDescriptionPtr DescriptionStructPtr;
Path::EDescType Type;
};
// This function pops off elements of InPath to go to the next level down in a Metasound description.
FDescriptionUnwindStep GoToNext(FDescPath& InPath, FDescriptionUnwindStep InElement);
};
// Utility class used by the frontend handle classes to access the underlying Description data they are modifying.
// Similar to a TSoftObjectPtr:
// this class caches a FDescPath to a specific element in a Metasound Class Description, and a weak pointer to a FDescriptionAccessPoint
// to use it with.
template <typename MetasoundDescriptionType>
class TDescriptionPtr
{
static constexpr bool bIsValidMetasoundDescriptionType =
TIsSame< MetasoundDescriptionType, FMetasoundDocument>::Value ||
TIsSame< MetasoundDescriptionType, FMetasoundGraphDescription>::Value ||
TIsSame< MetasoundDescriptionType, FMetasoundNodeDescription>::Value ||
TIsSame< MetasoundDescriptionType, FMetasoundInputDescription>::Value ||
TIsSame< MetasoundDescriptionType, FMetasoundOutputDescription>::Value ||
TIsSame< MetasoundDescriptionType, FMetasoundClassMetadata>::Value ||
TIsSame< MetasoundDescriptionType, FMetasoundClassDescription>::Value;
static_assert(bIsValidMetasoundDescriptionType, R"(Tried to use a Metasound::Frontend::FDescriptionAccessor with an invalid type. Supported types are:
FMetasoundDocument
FMetasoundGraphDescription,
FMetasoundNodeDescription,
FMetasoundInputDescription,
FMetasoundOutputDescription,
FMetasoundClassMetadata,
FMetasoundClassDescription)");
TWeakPtr<FDescriptionAccessPoint> AccessPoint;
FDescPath PathFromRoot;
public:
TDescriptionPtr() = delete;
TDescriptionPtr(TWeakPtr<FDescriptionAccessPoint> InAccessPoint, const FDescPath& InPathFromRoot)
: AccessPoint(InAccessPoint)
, PathFromRoot(InPathFromRoot)
{
}
// @returns false if the description this accessor was referencing has been destroyed,
// or if this was created with bad arguments.
bool IsValid() const
{
return AccessPoint.IsValid() && PathFromRoot.IsValid();
}
FDescPath GetPath() const
{
return PathFromRoot;
}
TWeakPtr<FDescriptionAccessPoint> GetAccessPoint() const
{
return AccessPoint;
}
// returns a pointer or reference to the description data struct.
// Note that this is NOT thread safe- this should be called on the same thread as whomever owns the lifecycle of the underlying MetasoundDescriptionType.
MetasoundDescriptionType* Get() const
{
if (TSharedPtr<FDescriptionAccessPoint> PinnedAccessPoint = AccessPoint.Pin())
{
if (TIsSame<MetasoundDescriptionType, FMetasoundDocument>::Value)
{
return (MetasoundDescriptionType*) &(PinnedAccessPoint->GetRoot());
}
else
{
return PinnedAccessPoint->GetElementAtPath<MetasoundDescriptionType>(PathFromRoot);
}
}
else
{
return nullptr;
}
}
MetasoundDescriptionType& GetChecked() const
{
MetasoundDescriptionType* ElementPtr = Get();
check(ElementPtr);
return *ElementPtr;
}
MetasoundDescriptionType& operator*() const
{
return GetChecked();
}
MetasoundDescriptionType* operator->() const
{
return Get();
}
FORCEINLINE explicit operator bool() const
{
return IsValid();
}
};
}
}