UnityGame/Library/PackageCache/com.unity.shadergraph/Editor/Drawing/Controllers/BlackboardController.cs

835 lines
37 KiB
C#
Raw Normal View History

2024-10-27 10:53:47 +03:00
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor.Experimental.GraphView;
using UnityEngine.UIElements;
using System;
using UnityEditor.Graphing;
using UnityEditor.ShaderGraph.Internal;
using GraphDataStore = UnityEditor.ShaderGraph.DataStore<UnityEditor.ShaderGraph.GraphData>;
using BlackboardItem = UnityEditor.ShaderGraph.Internal.ShaderInput;
namespace UnityEditor.ShaderGraph.Drawing
{
struct BlackboardShaderInputOrder
{
public bool isKeyword;
public bool isDropdown;
public KeywordType keywordType;
public ShaderKeyword builtInKeyword;
public string deprecatedPropertyName;
public int version;
}
class BlackboardShaderInputFactory
{
static public ShaderInput GetShaderInput(BlackboardShaderInputOrder order)
{
ShaderInput output;
if (order.isKeyword)
{
if (order.builtInKeyword == null)
{
output = new ShaderKeyword(order.keywordType);
}
else
{
output = order.builtInKeyword;
}
}
else if (order.isDropdown)
{
output = new ShaderDropdown();
}
else
{
switch (order.deprecatedPropertyName)
{
case "Color":
output = new ColorShaderProperty(order.version);
break;
default:
output = null;
AssertHelpers.Fail("BlackboardShaderInputFactory: Unknown deprecated property type.");
break;
}
}
return output;
}
}
class AddShaderInputAction : IGraphDataAction
{
public enum AddActionSource
{
Default,
AddMenu
}
void AddShaderInput(GraphData graphData)
{
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddShaderInputAction");
// If type property is valid, create instance of that type
if (blackboardItemType != null && blackboardItemType.IsSubclassOf(typeof(BlackboardItem)))
{
shaderInputReference = (BlackboardItem)Activator.CreateInstance(blackboardItemType, true);
}
else if (m_ShaderInputReferenceGetter != null)
{
shaderInputReference = m_ShaderInputReferenceGetter();
}
// If type is null a direct override object must have been provided or else we are in an error-state
else if (shaderInputReference == null)
{
AssertHelpers.Fail("BlackboardController: Unable to complete Add Shader Input action.");
return;
}
shaderInputReference.generatePropertyBlock = shaderInputReference.isExposable;
if (graphData.owner != null)
graphData.owner.RegisterCompleteObjectUndo("Add Shader Input");
else
AssertHelpers.Fail("GraphObject is null while carrying out AddShaderInputAction");
graphData.AddGraphInput(shaderInputReference);
// If no categoryToAddItemToGuid is provided, add the input to the default category
if (categoryToAddItemToGuid == String.Empty)
{
var defaultCategory = graphData.categories.FirstOrDefault();
AssertHelpers.IsNotNull(defaultCategory, "Default category reference is null.");
if (defaultCategory != null)
{
var addItemToCategoryAction = new AddItemToCategoryAction();
addItemToCategoryAction.categoryGuid = defaultCategory.categoryGuid;
addItemToCategoryAction.itemToAdd = shaderInputReference;
graphData.owner.graphDataStore.Dispatch(addItemToCategoryAction);
}
}
else
{
var addItemToCategoryAction = new AddItemToCategoryAction();
addItemToCategoryAction.categoryGuid = categoryToAddItemToGuid;
addItemToCategoryAction.itemToAdd = shaderInputReference;
graphData.owner.graphDataStore.Dispatch(addItemToCategoryAction);
}
}
public static AddShaderInputAction AddDeprecatedPropertyAction(BlackboardShaderInputOrder order)
{
return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
}
public static AddShaderInputAction AddDropdownAction(BlackboardShaderInputOrder order)
{
return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
}
public static AddShaderInputAction AddKeywordAction(BlackboardShaderInputOrder order)
{
return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
}
public static AddShaderInputAction AddPropertyAction(Type shaderInputType)
{
return new() { blackboardItemType = shaderInputType, addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
}
public Action<GraphData> modifyGraphDataAction => AddShaderInput;
// If this is a subclass of ShaderInput and is not null, then an object of this type is created to add to blackboard
// If the type field above is null and this is provided, then it is directly used as the item to add to blackboard
public BlackboardItem shaderInputReference { get; set; }
public AddActionSource addInputActionType { get; set; }
public string categoryToAddItemToGuid { get; set; } = String.Empty;
Type blackboardItemType { get; set; }
Func<BlackboardItem> m_ShaderInputReferenceGetter = null;
}
class ChangeGraphPathAction : IGraphDataAction
{
void ChangeGraphPath(GraphData graphData)
{
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out ChangeGraphPathAction");
graphData.path = NewGraphPath;
}
public Action<GraphData> modifyGraphDataAction => ChangeGraphPath;
public string NewGraphPath { get; set; }
}
class CopyShaderInputAction : IGraphDataAction
{
void CopyShaderInput(GraphData graphData)
{
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out CopyShaderInputAction");
AssertHelpers.IsNotNull(shaderInputToCopy, "ShaderInputToCopy is null while carrying out CopyShaderInputAction");
// Don't handle undo here as there are different contexts in which this action is used, that define the undo action
// TODO: Perhaps a sign that each of those need to be made their own actions instead of conflating intent into a single action
switch (shaderInputToCopy)
{
case AbstractShaderProperty property:
insertIndex = Mathf.Clamp(insertIndex, -1, graphData.properties.Count() - 1);
var copiedProperty = (AbstractShaderProperty)graphData.AddCopyOfShaderInput(property, insertIndex);
if (copiedProperty != null) // some property types cannot be duplicated (unknown types)
{
// Update the property nodes that depends on the copied node
foreach (var node in dependentNodeList)
{
if (node is PropertyNode propertyNode)
{
propertyNode.owner = graphData;
propertyNode.property = copiedProperty;
}
}
}
copiedShaderInput = copiedProperty;
break;
case ShaderKeyword shaderKeyword:
// InsertIndex gets passed in relative to the blackboard position of an item overall,
// and not relative to the array sizes of the properties/keywords/dropdowns
var keywordInsertIndex = insertIndex - graphData.properties.Count();
keywordInsertIndex = Mathf.Clamp(keywordInsertIndex, -1, graphData.keywords.Count() - 1);
// Don't duplicate built-in keywords within the same graph
if (shaderKeyword.isBuiltIn && graphData.keywords.Any(p => p.referenceName == shaderInputToCopy.referenceName))
return;
var copiedKeyword = (ShaderKeyword)graphData.AddCopyOfShaderInput(shaderKeyword, keywordInsertIndex);
// Update the keyword nodes that depends on the copied node
foreach (var node in dependentNodeList)
{
if (node is KeywordNode propertyNode)
{
propertyNode.owner = graphData;
propertyNode.keyword = copiedKeyword;
}
}
copiedShaderInput = copiedKeyword;
break;
case ShaderDropdown shaderDropdown:
// InsertIndex gets passed in relative to the blackboard position of an item overall,
// and not relative to the array sizes of the properties/keywords/dropdowns
var dropdownInsertIndex = insertIndex - graphData.properties.Count() - graphData.keywords.Count();
dropdownInsertIndex = Mathf.Clamp(dropdownInsertIndex, -1, graphData.dropdowns.Count() - 1);
var copiedDropdown = (ShaderDropdown)graphData.AddCopyOfShaderInput(shaderDropdown, dropdownInsertIndex);
// Update the dropdown nodes that depends on the copied node
foreach (var node in dependentNodeList)
{
if (node is DropdownNode propertyNode)
{
propertyNode.owner = graphData;
propertyNode.dropdown = copiedDropdown;
}
}
copiedShaderInput = copiedDropdown;
break;
default:
throw new ArgumentOutOfRangeException();
}
if (copiedShaderInput != null)
{
// If specific category to copy to is provided, find and use it
foreach (var category in graphData.categories)
{
if (category.categoryGuid == containingCategoryGuid)
{
// Ensures that the new item gets added after the item it was duplicated from
insertIndex += 1;
// If the source item was already the last item in list, just add to end of list
if (insertIndex >= category.childCount)
insertIndex = -1;
graphData.InsertItemIntoCategory(category.objectId, copiedShaderInput, insertIndex);
return;
}
}
// Else, add to default category
graphData.categories.First().InsertItemIntoCategory(copiedShaderInput);
}
}
public Action<GraphData> modifyGraphDataAction => CopyShaderInput;
public IEnumerable<AbstractMaterialNode> dependentNodeList { get; set; } = new List<AbstractMaterialNode>();
public BlackboardItem shaderInputToCopy { get; set; }
public BlackboardItem copiedShaderInput { get; set; }
public string containingCategoryGuid { get; set; }
public int insertIndex { get; set; } = -1;
}
class AddCategoryAction : IGraphDataAction
{
void AddCategory(GraphData graphData)
{
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddCategoryAction");
graphData.owner.RegisterCompleteObjectUndo("Add Category");
// If categoryDataReference is not null, directly add it to graphData
if (categoryDataReference == null)
categoryDataReference = new CategoryData(categoryName, childObjects);
graphData.AddCategory(categoryDataReference);
}
public Action<GraphData> modifyGraphDataAction => AddCategory;
// Direct reference to the categoryData to use if it is specified
public CategoryData categoryDataReference { get; set; }
public string categoryName { get; set; } = String.Empty;
public List<ShaderInput> childObjects { get; set; }
}
class MoveCategoryAction : IGraphDataAction
{
void MoveCategory(GraphData graphData)
{
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out MoveCategoryAction");
graphData.owner.RegisterCompleteObjectUndo("Move Category");
// Handling for out of range moves is slightly different, but otherwise we need to reverse for insertion order.
var guids = newIndexValue >= graphData.categories.Count() ? categoryGuids : categoryGuids.Reverse<string>();
foreach (var guid in categoryGuids)
{
var cat = graphData.categories.FirstOrDefault(c => c.categoryGuid == guid);
graphData.MoveCategory(cat, newIndexValue);
}
}
public Action<GraphData> modifyGraphDataAction => MoveCategory;
// Reference to the shader input being modified
internal List<string> categoryGuids { get; set; }
internal int newIndexValue { get; set; }
}
class AddItemToCategoryAction : IGraphDataAction
{
public enum AddActionSource
{
Default,
DragDrop
}
void AddItemsToCategory(GraphData graphData)
{
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddItemToCategoryAction");
graphData.owner.RegisterCompleteObjectUndo("Add Item to Category");
graphData.InsertItemIntoCategory(categoryGuid, itemToAdd, indexToAddItemAt);
}
public Action<GraphData> modifyGraphDataAction => AddItemsToCategory;
public string categoryGuid { get; set; }
public ShaderInput itemToAdd { get; set; }
// By default an item is always added to the end of a category, if this value is set to something other than -1, will insert the item at that position within the category
public int indexToAddItemAt { get; set; } = -1;
public AddActionSource addActionSource { get; set; }
}
class CopyCategoryAction : IGraphDataAction
{
void CopyCategory(GraphData graphData)
{
AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out CopyCategoryAction");
AssertHelpers.IsNotNull(categoryToCopyReference, "CategoryToCopyReference is null while carrying out CopyCategoryAction");
// This is called by MaterialGraphView currently, no need to repeat it here, though ideally it would live here
//graphData.owner.RegisterCompleteObjectUndo("Copy Category");
newCategoryDataReference = graphData.CopyCategory(categoryToCopyReference);
}
// Reference to the new category created as a copy
public CategoryData newCategoryDataReference { get; set; }
// After category has been copied, store reference to it
public CategoryData categoryToCopyReference { get; set; }
public Action<GraphData> modifyGraphDataAction => CopyCategory;
}
class ShaderVariantLimitAction : IGraphDataAction
{
public int currentVariantCount { get; set; } = 0;
public int maxVariantCount { get; set; } = 0;
public ShaderVariantLimitAction(int currentVariantCount, int maxVariantCount)
{
this.maxVariantCount = maxVariantCount;
this.currentVariantCount = currentVariantCount;
}
// There's no action actually performed on the graph, but we need to implement this as a valid function
public Action<GraphData> modifyGraphDataAction => Empty;
void Empty(GraphData graphData)
{
}
}
class BlackboardController : SGViewController<GraphData, BlackboardViewModel>
{
// Type changes (adds/removes of Types) only happen after a full assembly reload so its safe to make this static
static IList<Type> s_ShaderInputTypes;
static BlackboardController()
{
var shaderInputTypes = TypeCache.GetTypesWithAttribute<BlackboardInputInfo>().ToList();
// Sort the ShaderInput by priority using the BlackboardInputInfo attribute
shaderInputTypes.Sort((s1, s2) =>
{
var info1 = Attribute.GetCustomAttribute(s1, typeof(BlackboardInputInfo)) as BlackboardInputInfo;
var info2 = Attribute.GetCustomAttribute(s2, typeof(BlackboardInputInfo)) as BlackboardInputInfo;
if (info1.priority == info2.priority)
return (info1.name ?? s1.Name).CompareTo(info2.name ?? s2.Name);
else
return info1.priority.CompareTo(info2.priority);
});
s_ShaderInputTypes = shaderInputTypes.ToList();
}
BlackboardCategoryController m_DefaultCategoryController = null;
Dictionary<string, BlackboardCategoryController> m_BlackboardCategoryControllers = new Dictionary<string, BlackboardCategoryController>();
protected SGBlackboard m_Blackboard;
internal SGBlackboard blackboard
{
get => m_Blackboard;
private set => m_Blackboard = value;
}
public string GetFirstSelectedCategoryGuid()
{
if (m_Blackboard == null)
{
return string.Empty;
}
var copiedSelectionList = new List<ISelectable>(m_Blackboard.selection);
var selectedCategories = new List<SGBlackboardCategory>();
var selectedCategoryGuid = String.Empty;
for (int i = 0; i < copiedSelectionList.Count; i++)
{
var selectable = copiedSelectionList[i];
if (selectable is SGBlackboardCategory category)
{
selectedCategories.Add(selectable as SGBlackboardCategory);
}
}
if (selectedCategories.Any())
{
selectedCategoryGuid = selectedCategories[0].viewModel.associatedCategoryGuid;
}
return selectedCategoryGuid;
}
void InitializeViewModel(bool useDropdowns)
{
// Clear the view model
ViewModel.ResetViewModelData();
ViewModel.subtitle = BlackboardUtils.FormatPath(Model.path);
BlackboardShaderInputOrder propertyTypesOrder = new BlackboardShaderInputOrder();
// Property data first
foreach (var shaderInputType in s_ShaderInputTypes)
{
if (shaderInputType.IsAbstract)
continue;
var info = Attribute.GetCustomAttribute(shaderInputType, typeof(BlackboardInputInfo)) as BlackboardInputInfo;
string name = info?.name ?? ObjectNames.NicifyVariableName(shaderInputType.Name.Replace("ShaderProperty", ""));
// QUICK FIX TO DEAL WITH DEPRECATED COLOR PROPERTY
if (name.Equals("Color", StringComparison.InvariantCultureIgnoreCase) && ShaderGraphPreferences.allowDeprecatedBehaviors)
{
propertyTypesOrder.isKeyword = false;
propertyTypesOrder.deprecatedPropertyName = name;
propertyTypesOrder.version = ColorShaderProperty.deprecatedVersion;
ViewModel.propertyNameToAddActionMap.Add($"Color (Legacy v0)", AddShaderInputAction.AddDeprecatedPropertyAction(propertyTypesOrder));
ViewModel.propertyNameToAddActionMap.Add(name, AddShaderInputAction.AddPropertyAction(shaderInputType));
}
else
ViewModel.propertyNameToAddActionMap.Add(name, AddShaderInputAction.AddPropertyAction(shaderInputType));
}
// Default Keywords next
BlackboardShaderInputOrder keywordTypesOrder = new BlackboardShaderInputOrder();
keywordTypesOrder.isKeyword = true;
keywordTypesOrder.keywordType = KeywordType.Boolean;
ViewModel.defaultKeywordNameToAddActionMap.Add("Boolean", AddShaderInputAction.AddKeywordAction(keywordTypesOrder));
keywordTypesOrder.keywordType = KeywordType.Enum;
ViewModel.defaultKeywordNameToAddActionMap.Add("Enum", AddShaderInputAction.AddKeywordAction(keywordTypesOrder));
// Built-In Keywords after that
foreach (var builtinKeywordDescriptor in KeywordUtil.GetBuiltinKeywordDescriptors())
{
var keyword = ShaderKeyword.CreateBuiltInKeyword(builtinKeywordDescriptor);
// Do not allow user to add built-in keywords that conflict with user-made keywords that have the same reference name or display name
if (Model.keywords.Any(x => x.referenceName == keyword.referenceName || x.displayName == keyword.displayName))
{
ViewModel.disabledKeywordNameList.Add(keyword.displayName);
}
else
{
keywordTypesOrder.builtInKeyword = (ShaderKeyword)keyword.Copy();
ViewModel.builtInKeywordNameToAddActionMap.Add(keyword.displayName, AddShaderInputAction.AddKeywordAction(keywordTypesOrder));
}
}
if (useDropdowns)
{
BlackboardShaderInputOrder dropdownsOrder = new BlackboardShaderInputOrder();
dropdownsOrder.isDropdown = true;
ViewModel.defaultDropdownNameToAdd = new Tuple<string, IGraphDataAction>("Dropdown", AddShaderInputAction.AddDropdownAction(dropdownsOrder));
}
// Category data last
var defaultNewCategoryReference = new CategoryData("Category");
ViewModel.addCategoryAction = new AddCategoryAction() { categoryDataReference = defaultNewCategoryReference };
ViewModel.requestModelChangeAction = this.RequestModelChange;
ViewModel.categoryInfoList.AddRange(DataStore.State.categories.ToList());
}
internal BlackboardController(GraphData model, BlackboardViewModel inViewModel, GraphDataStore graphDataStore)
: base(model, inViewModel, graphDataStore)
{
// TODO: hide this more generically for category types.
bool useDropdowns = model.isSubGraph;
InitializeViewModel(useDropdowns);
blackboard = new SGBlackboard(ViewModel, this);
// Add default category at the top of the blackboard (create it if it doesn't exist already)
var existingDefaultCategory = DataStore.State.categories.FirstOrDefault();
if (existingDefaultCategory != null && existingDefaultCategory.IsNamedCategory() == false)
{
AddBlackboardCategory(graphDataStore, existingDefaultCategory);
}
else
{
// Any properties that don't already have a category (for example, if this graph is being loaded from an older version that doesn't have category data)
var uncategorizedBlackboardItems = new List<ShaderInput>();
foreach (var shaderProperty in DataStore.State.properties)
if (IsInputUncategorized(shaderProperty))
uncategorizedBlackboardItems.Add(shaderProperty);
foreach (var shaderKeyword in DataStore.State.keywords)
if (IsInputUncategorized(shaderKeyword))
uncategorizedBlackboardItems.Add(shaderKeyword);
if (useDropdowns)
{
foreach (var shaderDropdown in DataStore.State.dropdowns)
if (IsInputUncategorized(shaderDropdown))
uncategorizedBlackboardItems.Add(shaderDropdown);
}
var addCategoryAction = new AddCategoryAction();
addCategoryAction.categoryDataReference = CategoryData.DefaultCategory(uncategorizedBlackboardItems);
graphDataStore.Dispatch(addCategoryAction);
}
// Get the reference to default category controller after its been added
m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault();
AssertHelpers.IsNotNull(m_DefaultCategoryController, "Failed to instantiate default category.");
// Handle loaded-in categories from graph first, skipping the first/default category
foreach (var categoryData in ViewModel.categoryInfoList.Skip(1))
{
AddBlackboardCategory(graphDataStore, categoryData);
}
}
internal string editorPrefsBaseKey => "unity.shadergraph." + DataStore.State.objectId;
BlackboardCategoryController AddBlackboardCategory(GraphDataStore graphDataStore, CategoryData categoryInfo)
{
var blackboardCategoryViewModel = new BlackboardCategoryViewModel();
blackboardCategoryViewModel.parentView = blackboard;
blackboardCategoryViewModel.requestModelChangeAction = ViewModel.requestModelChangeAction;
blackboardCategoryViewModel.name = categoryInfo.name;
blackboardCategoryViewModel.associatedCategoryGuid = categoryInfo.categoryGuid;
blackboardCategoryViewModel.isExpanded = EditorPrefs.GetBool($"{editorPrefsBaseKey}.{categoryInfo.categoryGuid}.{ChangeCategoryIsExpandedAction.kEditorPrefKey}", true);
var blackboardCategoryController = new BlackboardCategoryController(categoryInfo, blackboardCategoryViewModel, graphDataStore);
if (m_BlackboardCategoryControllers.ContainsKey(categoryInfo.categoryGuid) == false)
{
m_BlackboardCategoryControllers.Add(categoryInfo.categoryGuid, blackboardCategoryController);
m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault();
}
else
{
AssertHelpers.Fail("Failed to add category controller due to category with same GUID already having been added.");
return null;
}
return blackboardCategoryController;
}
// Creates controller, view and view model for a blackboard item and adds the view to the specified index in the category
SGBlackboardRow InsertBlackboardRow(BlackboardItem shaderInput, int insertionIndex = -1)
{
return m_DefaultCategoryController.InsertBlackboardRow(shaderInput, insertionIndex);
}
public void UpdateBlackboardTitle(string newTitle)
{
ViewModel.title = newTitle;
blackboard.title = ViewModel.title;
}
protected override void RequestModelChange(IGraphDataAction changeAction)
{
DataStore.Dispatch(changeAction);
}
// Called by GraphDataStore.Subscribe after the model has been changed
protected override void ModelChanged(GraphData graphData, IGraphDataAction changeAction)
{
// Reconstruct view-model first
// TODO: hide this more generically for category types.
bool useDropdowns = graphData.isSubGraph;
InitializeViewModel(useDropdowns);
var graphView = ViewModel.parentView as MaterialGraphView;
switch (changeAction)
{
// If newly added input doesn't belong to any of the user-made categories, add it to the default category at top of blackboard
case AddShaderInputAction addBlackboardItemAction:
if (IsInputUncategorized(addBlackboardItemAction.shaderInputReference))
{
var blackboardRow = InsertBlackboardRow(addBlackboardItemAction.shaderInputReference);
if (blackboardRow != null)
{
var propertyView = blackboardRow.Q<SGBlackboardField>();
if (addBlackboardItemAction.addInputActionType == AddShaderInputAction.AddActionSource.AddMenu)
propertyView.OpenTextEditor();
}
}
break;
// Need to handle deletion of shader inputs here as opposed to BlackboardCategoryController, as currently,
// once removed from the categories there is no way to associate an input with the category that owns it
case DeleteShaderInputAction deleteShaderInputAction:
foreach (var shaderInput in deleteShaderInputAction.shaderInputsToDelete)
RemoveInputFromBlackboard(shaderInput);
break;
case HandleUndoRedoAction handleUndoRedoAction:
ClearBlackboardCategories();
foreach (var categoryData in graphData.addedCategories)
AddBlackboardCategory(DataStore, categoryData);
m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault();
break;
case CopyShaderInputAction copyShaderInputAction:
// In the specific case of only-one keywords like Material Quality and Raytracing, they can get copied, but because only one can exist, the output copied value is null
if (copyShaderInputAction.copiedShaderInput != null && IsInputUncategorized(copyShaderInputAction.copiedShaderInput))
{
var blackboardRow = InsertBlackboardRow(copyShaderInputAction.copiedShaderInput, copyShaderInputAction.insertIndex);
var propertyView = blackboardRow.Q<SGBlackboardField>();
graphView?.AddToSelectionNoUndoRecord(propertyView);
}
break;
case AddCategoryAction addCategoryAction:
AddBlackboardCategory(DataStore, addCategoryAction.categoryDataReference);
// Iterate through anything that is selected currently
foreach (var selectedElement in blackboard.selection.ToList())
{
if (selectedElement is SGBlackboardField { userData: ShaderInput shaderInput })
{
// If a blackboard item is selected, first remove it from the blackboard
RemoveInputFromBlackboard(shaderInput);
// Then add input to the new category
var addItemToCategoryAction = new AddItemToCategoryAction();
addItemToCategoryAction.categoryGuid = addCategoryAction.categoryDataReference.categoryGuid;
addItemToCategoryAction.itemToAdd = shaderInput;
DataStore.Dispatch(addItemToCategoryAction);
}
}
break;
case DeleteCategoryAction deleteCategoryAction:
// Clean up deleted categories
foreach (var categoryGUID in deleteCategoryAction.categoriesToRemoveGuids)
{
RemoveBlackboardCategory(categoryGUID);
}
break;
case MoveCategoryAction moveCategoryAction:
ClearBlackboardCategories();
foreach (var categoryData in ViewModel.categoryInfoList)
AddBlackboardCategory(graphData.owner.graphDataStore, categoryData);
break;
case CopyCategoryAction copyCategoryAction:
var blackboardCategory = AddBlackboardCategory(graphData.owner.graphDataStore, copyCategoryAction.newCategoryDataReference);
if (blackboardCategory != null)
graphView?.AddToSelectionNoUndoRecord(blackboardCategory.blackboardCategoryView);
break;
case ShaderVariantLimitAction shaderVariantLimitAction:
blackboard.SetCurrentVariantUsage(shaderVariantLimitAction.currentVariantCount, shaderVariantLimitAction.maxVariantCount);
break;
}
// Lets all event handlers this controller owns/manages know that the model has changed
// Usually this is to update views and make them reconstruct themself from updated view-model
//NotifyChange(changeAction);
// Let child controllers know about changes to this controller so they may update themselves in turn
//ApplyChanges();
}
void RemoveInputFromBlackboard(ShaderInput shaderInput)
{
// Check if input is in one of the categories
foreach (var controller in m_BlackboardCategoryControllers.Values)
{
var blackboardRow = controller.FindBlackboardRow(shaderInput);
if (blackboardRow != null)
{
controller.RemoveBlackboardRow(shaderInput);
return;
}
}
}
bool IsInputUncategorized(ShaderInput shaderInput)
{
// Skip the first category controller as that is guaranteed to be the default category
foreach (var categoryController in m_BlackboardCategoryControllers.Values.Skip(1))
{
if (categoryController.IsInputInCategory(shaderInput))
return false;
}
return true;
}
public SGBlackboardCategory GetBlackboardCategory(string inputGuid)
{
foreach (var categoryController in m_BlackboardCategoryControllers.Values)
{
if (categoryController.Model.categoryGuid == inputGuid)
return categoryController.blackboardCategoryView;
}
return null;
}
public SGBlackboardRow GetBlackboardRow(ShaderInput blackboardItem)
{
foreach (var categoryController in m_BlackboardCategoryControllers.Values)
{
var blackboardRow = categoryController.FindBlackboardRow(blackboardItem);
if (blackboardRow != null)
return blackboardRow;
}
return null;
}
int numberOfCategories => m_BlackboardCategoryControllers.Count;
// Gets the index after the currently selected shader input for pasting properties into this graph
internal int GetInsertionIndexForPaste()
{
if (blackboard?.selection == null || blackboard.selection.Count == 0)
{
return 0;
}
foreach (ISelectable selection in blackboard.selection)
{
if (selection is SGBlackboardField blackboardPropertyView)
{
SGBlackboardRow row = blackboardPropertyView.GetFirstAncestorOfType<SGBlackboardRow>();
SGBlackboardCategory category = blackboardPropertyView.GetFirstAncestorOfType<SGBlackboardCategory>();
if (row == null || category == null)
continue;
int blackboardFieldIndex = category.IndexOf(row);
return blackboardFieldIndex;
}
}
return 0;
}
void RemoveBlackboardCategory(string categoryGUID)
{
m_BlackboardCategoryControllers.TryGetValue(categoryGUID, out var blackboardCategoryController);
if (blackboardCategoryController != null)
{
blackboardCategoryController.Dispose();
m_BlackboardCategoryControllers.Remove(categoryGUID);
}
else
AssertHelpers.Fail("Tried to remove a category that doesn't exist. ");
}
public override void Dispose()
{
if (m_Blackboard == null)
return;
base.Dispose();
m_DefaultCategoryController = null;
ClearBlackboardCategories();
m_Blackboard?.Dispose();
m_Blackboard = null;
}
void ClearBlackboardCategories()
{
foreach (var categoryController in m_BlackboardCategoryControllers.Values)
{
categoryController.Dispose();
}
m_BlackboardCategoryControllers.Clear();
}
// Meant to be used by UI testing in order to clear blackboard state
internal void ResetBlackboardState()
{
ClearBlackboardCategories();
var addCategoryAction = new AddCategoryAction();
addCategoryAction.categoryDataReference = CategoryData.DefaultCategory();
DataStore.Dispatch(addCategoryAction);
}
}
}