Files
UnrealEngineUWP/Engine/Plugins/Runtime/Metasound/Source/MetasoundFrontend/Public/MetasoundFrontendBaseClasses.h
Rob Gay bdc971b2d0 Move UMetasound to MetasoundEngine (step toward editor enablement as EdGraph reference will be required on class).
#rb ethan.geller
#jira UEAU-476

[CL 13860810 by Rob Gay in ue5-main branch]
2020-07-15 00:16:40 -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 METASOUNDFRONTEND_API 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);
}
using FClassDependencyIDs = TArray<uint32>;
/**
* 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 METASOUNDFRONTEND_API 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();
}
};
}
}