A game about forced loneliness, made by TACStudios
1using System.Collections.Generic;
2using System.Linq;
3using UnityEngine;
4using UnityEditor.Experimental.GraphView;
5using UnityEngine.UIElements;
6using System;
7using UnityEditor.Graphing;
8using UnityEditor.ShaderGraph.Internal;
9using GraphDataStore = UnityEditor.ShaderGraph.DataStore<UnityEditor.ShaderGraph.GraphData>;
10using BlackboardItem = UnityEditor.ShaderGraph.Internal.ShaderInput;
11
12namespace UnityEditor.ShaderGraph.Drawing
13{
14 struct BlackboardShaderInputOrder
15 {
16 public bool isKeyword;
17 public bool isDropdown;
18 public KeywordType keywordType;
19 public ShaderKeyword builtInKeyword;
20 public string deprecatedPropertyName;
21 public int version;
22 }
23 class BlackboardShaderInputFactory
24 {
25 static public ShaderInput GetShaderInput(BlackboardShaderInputOrder order)
26 {
27 ShaderInput output;
28 if (order.isKeyword)
29 {
30 if (order.builtInKeyword == null)
31 {
32 output = new ShaderKeyword(order.keywordType);
33 }
34 else
35 {
36 output = order.builtInKeyword;
37 }
38 }
39 else if (order.isDropdown)
40 {
41 output = new ShaderDropdown();
42 }
43 else
44 {
45 switch (order.deprecatedPropertyName)
46 {
47 case "Color":
48 output = new ColorShaderProperty(order.version);
49 break;
50 default:
51 output = null;
52 AssertHelpers.Fail("BlackboardShaderInputFactory: Unknown deprecated property type.");
53 break;
54 }
55 }
56
57 return output;
58 }
59 }
60 class AddShaderInputAction : IGraphDataAction
61 {
62 public enum AddActionSource
63 {
64 Default,
65 AddMenu
66 }
67
68 void AddShaderInput(GraphData graphData)
69 {
70 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddShaderInputAction");
71
72 // If type property is valid, create instance of that type
73 if (blackboardItemType != null && blackboardItemType.IsSubclassOf(typeof(BlackboardItem)))
74 {
75 shaderInputReference = (BlackboardItem)Activator.CreateInstance(blackboardItemType, true);
76 }
77 else if (m_ShaderInputReferenceGetter != null)
78 {
79 shaderInputReference = m_ShaderInputReferenceGetter();
80 }
81 // If type is null a direct override object must have been provided or else we are in an error-state
82 else if (shaderInputReference == null)
83 {
84 AssertHelpers.Fail("BlackboardController: Unable to complete Add Shader Input action.");
85 return;
86 }
87
88 shaderInputReference.generatePropertyBlock = shaderInputReference.isExposable;
89
90 if (graphData.owner != null)
91 graphData.owner.RegisterCompleteObjectUndo("Add Shader Input");
92 else
93 AssertHelpers.Fail("GraphObject is null while carrying out AddShaderInputAction");
94
95 graphData.AddGraphInput(shaderInputReference);
96
97 // If no categoryToAddItemToGuid is provided, add the input to the default category
98 if (categoryToAddItemToGuid == String.Empty)
99 {
100 var defaultCategory = graphData.categories.FirstOrDefault();
101 AssertHelpers.IsNotNull(defaultCategory, "Default category reference is null.");
102 if (defaultCategory != null)
103 {
104 var addItemToCategoryAction = new AddItemToCategoryAction();
105 addItemToCategoryAction.categoryGuid = defaultCategory.categoryGuid;
106 addItemToCategoryAction.itemToAdd = shaderInputReference;
107 graphData.owner.graphDataStore.Dispatch(addItemToCategoryAction);
108 }
109 }
110 else
111 {
112 var addItemToCategoryAction = new AddItemToCategoryAction();
113 addItemToCategoryAction.categoryGuid = categoryToAddItemToGuid;
114 addItemToCategoryAction.itemToAdd = shaderInputReference;
115 graphData.owner.graphDataStore.Dispatch(addItemToCategoryAction);
116 }
117 }
118
119 public static AddShaderInputAction AddDeprecatedPropertyAction(BlackboardShaderInputOrder order)
120 {
121 return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
122 }
123
124 public static AddShaderInputAction AddDropdownAction(BlackboardShaderInputOrder order)
125 {
126 return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
127 }
128
129 public static AddShaderInputAction AddKeywordAction(BlackboardShaderInputOrder order)
130 {
131 return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
132 }
133
134 public static AddShaderInputAction AddPropertyAction(Type shaderInputType)
135 {
136 return new() { blackboardItemType = shaderInputType, addInputActionType = AddShaderInputAction.AddActionSource.AddMenu };
137 }
138
139 public Action<GraphData> modifyGraphDataAction => AddShaderInput;
140 // If this is a subclass of ShaderInput and is not null, then an object of this type is created to add to blackboard
141 // If the type field above is null and this is provided, then it is directly used as the item to add to blackboard
142 public BlackboardItem shaderInputReference { get; set; }
143 public AddActionSource addInputActionType { get; set; }
144 public string categoryToAddItemToGuid { get; set; } = String.Empty;
145
146 Type blackboardItemType { get; set; }
147
148 Func<BlackboardItem> m_ShaderInputReferenceGetter = null;
149 }
150
151 class ChangeGraphPathAction : IGraphDataAction
152 {
153 void ChangeGraphPath(GraphData graphData)
154 {
155 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out ChangeGraphPathAction");
156 graphData.path = NewGraphPath;
157 }
158
159 public Action<GraphData> modifyGraphDataAction => ChangeGraphPath;
160
161 public string NewGraphPath { get; set; }
162 }
163
164 class CopyShaderInputAction : IGraphDataAction
165 {
166 void CopyShaderInput(GraphData graphData)
167 {
168 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out CopyShaderInputAction");
169 AssertHelpers.IsNotNull(shaderInputToCopy, "ShaderInputToCopy is null while carrying out CopyShaderInputAction");
170
171 // Don't handle undo here as there are different contexts in which this action is used, that define the undo action
172 // TODO: Perhaps a sign that each of those need to be made their own actions instead of conflating intent into a single action
173
174 switch (shaderInputToCopy)
175 {
176 case AbstractShaderProperty property:
177
178 insertIndex = Mathf.Clamp(insertIndex, -1, graphData.properties.Count() - 1);
179 var copiedProperty = (AbstractShaderProperty)graphData.AddCopyOfShaderInput(property, insertIndex);
180 if (copiedProperty != null) // some property types cannot be duplicated (unknown types)
181 {
182 // Update the property nodes that depends on the copied node
183 foreach (var node in dependentNodeList)
184 {
185 if (node is PropertyNode propertyNode)
186 {
187 propertyNode.owner = graphData;
188 propertyNode.property = copiedProperty;
189 }
190 }
191 }
192
193
194 copiedShaderInput = copiedProperty;
195 break;
196
197 case ShaderKeyword shaderKeyword:
198 // InsertIndex gets passed in relative to the blackboard position of an item overall,
199 // and not relative to the array sizes of the properties/keywords/dropdowns
200 var keywordInsertIndex = insertIndex - graphData.properties.Count();
201
202 keywordInsertIndex = Mathf.Clamp(keywordInsertIndex, -1, graphData.keywords.Count() - 1);
203
204 // Don't duplicate built-in keywords within the same graph
205 if (shaderKeyword.isBuiltIn && graphData.keywords.Any(p => p.referenceName == shaderInputToCopy.referenceName))
206 return;
207
208 var copiedKeyword = (ShaderKeyword)graphData.AddCopyOfShaderInput(shaderKeyword, keywordInsertIndex);
209
210 // Update the keyword nodes that depends on the copied node
211 foreach (var node in dependentNodeList)
212 {
213 if (node is KeywordNode propertyNode)
214 {
215 propertyNode.owner = graphData;
216 propertyNode.keyword = copiedKeyword;
217 }
218 }
219
220 copiedShaderInput = copiedKeyword;
221 break;
222
223 case ShaderDropdown shaderDropdown:
224 // InsertIndex gets passed in relative to the blackboard position of an item overall,
225 // and not relative to the array sizes of the properties/keywords/dropdowns
226 var dropdownInsertIndex = insertIndex - graphData.properties.Count() - graphData.keywords.Count();
227
228 dropdownInsertIndex = Mathf.Clamp(dropdownInsertIndex, -1, graphData.dropdowns.Count() - 1);
229
230 var copiedDropdown = (ShaderDropdown)graphData.AddCopyOfShaderInput(shaderDropdown, dropdownInsertIndex);
231
232 // Update the dropdown nodes that depends on the copied node
233 foreach (var node in dependentNodeList)
234 {
235 if (node is DropdownNode propertyNode)
236 {
237 propertyNode.owner = graphData;
238 propertyNode.dropdown = copiedDropdown;
239 }
240 }
241
242 copiedShaderInput = copiedDropdown;
243 break;
244
245 default:
246 throw new ArgumentOutOfRangeException();
247 }
248
249 if (copiedShaderInput != null)
250 {
251 // If specific category to copy to is provided, find and use it
252 foreach (var category in graphData.categories)
253 {
254 if (category.categoryGuid == containingCategoryGuid)
255 {
256 // Ensures that the new item gets added after the item it was duplicated from
257 insertIndex += 1;
258 // If the source item was already the last item in list, just add to end of list
259 if (insertIndex >= category.childCount)
260 insertIndex = -1;
261 graphData.InsertItemIntoCategory(category.objectId, copiedShaderInput, insertIndex);
262 return;
263 }
264 }
265
266 // Else, add to default category
267 graphData.categories.First().InsertItemIntoCategory(copiedShaderInput);
268 }
269 }
270
271 public Action<GraphData> modifyGraphDataAction => CopyShaderInput;
272
273 public IEnumerable<AbstractMaterialNode> dependentNodeList { get; set; } = new List<AbstractMaterialNode>();
274
275 public BlackboardItem shaderInputToCopy { get; set; }
276
277 public BlackboardItem copiedShaderInput { get; set; }
278
279 public string containingCategoryGuid { get; set; }
280
281 public int insertIndex { get; set; } = -1;
282 }
283
284 class AddCategoryAction : IGraphDataAction
285 {
286 void AddCategory(GraphData graphData)
287 {
288 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddCategoryAction");
289 graphData.owner.RegisterCompleteObjectUndo("Add Category");
290 // If categoryDataReference is not null, directly add it to graphData
291 if (categoryDataReference == null)
292 categoryDataReference = new CategoryData(categoryName, childObjects);
293 graphData.AddCategory(categoryDataReference);
294 }
295
296 public Action<GraphData> modifyGraphDataAction => AddCategory;
297
298 // Direct reference to the categoryData to use if it is specified
299 public CategoryData categoryDataReference { get; set; }
300 public string categoryName { get; set; } = String.Empty;
301 public List<ShaderInput> childObjects { get; set; }
302 }
303
304 class MoveCategoryAction : IGraphDataAction
305 {
306 void MoveCategory(GraphData graphData)
307 {
308 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out MoveCategoryAction");
309 graphData.owner.RegisterCompleteObjectUndo("Move Category");
310 // Handling for out of range moves is slightly different, but otherwise we need to reverse for insertion order.
311 var guids = newIndexValue >= graphData.categories.Count() ? categoryGuids : categoryGuids.Reverse<string>();
312 foreach (var guid in categoryGuids)
313 {
314 var cat = graphData.categories.FirstOrDefault(c => c.categoryGuid == guid);
315 graphData.MoveCategory(cat, newIndexValue);
316 }
317 }
318
319 public Action<GraphData> modifyGraphDataAction => MoveCategory;
320
321 // Reference to the shader input being modified
322 internal List<string> categoryGuids { get; set; }
323
324 internal int newIndexValue { get; set; }
325 }
326
327 class AddItemToCategoryAction : IGraphDataAction
328 {
329 public enum AddActionSource
330 {
331 Default,
332 DragDrop
333 }
334
335 void AddItemsToCategory(GraphData graphData)
336 {
337 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddItemToCategoryAction");
338 graphData.owner.RegisterCompleteObjectUndo("Add Item to Category");
339 graphData.InsertItemIntoCategory(categoryGuid, itemToAdd, indexToAddItemAt);
340 }
341
342 public Action<GraphData> modifyGraphDataAction => AddItemsToCategory;
343
344 public string categoryGuid { get; set; }
345
346 public ShaderInput itemToAdd { get; set; }
347
348 // 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
349 public int indexToAddItemAt { get; set; } = -1;
350
351 public AddActionSource addActionSource { get; set; }
352 }
353
354 class CopyCategoryAction : IGraphDataAction
355 {
356 void CopyCategory(GraphData graphData)
357 {
358 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out CopyCategoryAction");
359 AssertHelpers.IsNotNull(categoryToCopyReference, "CategoryToCopyReference is null while carrying out CopyCategoryAction");
360
361 // This is called by MaterialGraphView currently, no need to repeat it here, though ideally it would live here
362 //graphData.owner.RegisterCompleteObjectUndo("Copy Category");
363
364 newCategoryDataReference = graphData.CopyCategory(categoryToCopyReference);
365 }
366
367 // Reference to the new category created as a copy
368 public CategoryData newCategoryDataReference { get; set; }
369
370 // After category has been copied, store reference to it
371 public CategoryData categoryToCopyReference { get; set; }
372
373 public Action<GraphData> modifyGraphDataAction => CopyCategory;
374 }
375
376 class ShaderVariantLimitAction : IGraphDataAction
377 {
378 public int currentVariantCount { get; set; } = 0;
379 public int maxVariantCount { get; set; } = 0;
380
381 public ShaderVariantLimitAction(int currentVariantCount, int maxVariantCount)
382 {
383 this.maxVariantCount = maxVariantCount;
384 this.currentVariantCount = currentVariantCount;
385 }
386
387 // There's no action actually performed on the graph, but we need to implement this as a valid function
388 public Action<GraphData> modifyGraphDataAction => Empty;
389
390 void Empty(GraphData graphData)
391 {
392 }
393 }
394
395 class BlackboardController : SGViewController<GraphData, BlackboardViewModel>
396 {
397 // Type changes (adds/removes of Types) only happen after a full assembly reload so its safe to make this static
398 static IList<Type> s_ShaderInputTypes;
399
400 static BlackboardController()
401 {
402 var shaderInputTypes = TypeCache.GetTypesWithAttribute<BlackboardInputInfo>().ToList();
403 // Sort the ShaderInput by priority using the BlackboardInputInfo attribute
404 shaderInputTypes.Sort((s1, s2) =>
405 {
406 var info1 = Attribute.GetCustomAttribute(s1, typeof(BlackboardInputInfo)) as BlackboardInputInfo;
407 var info2 = Attribute.GetCustomAttribute(s2, typeof(BlackboardInputInfo)) as BlackboardInputInfo;
408
409 if (info1.priority == info2.priority)
410 return (info1.name ?? s1.Name).CompareTo(info2.name ?? s2.Name);
411 else
412 return info1.priority.CompareTo(info2.priority);
413 });
414
415 s_ShaderInputTypes = shaderInputTypes.ToList();
416 }
417
418 BlackboardCategoryController m_DefaultCategoryController = null;
419 Dictionary<string, BlackboardCategoryController> m_BlackboardCategoryControllers = new Dictionary<string, BlackboardCategoryController>();
420
421 protected SGBlackboard m_Blackboard;
422
423 internal SGBlackboard blackboard
424 {
425 get => m_Blackboard;
426 private set => m_Blackboard = value;
427 }
428 public string GetFirstSelectedCategoryGuid()
429 {
430 if (m_Blackboard == null)
431 {
432 return string.Empty;
433 }
434 var copiedSelectionList = new List<ISelectable>(m_Blackboard.selection);
435 var selectedCategories = new List<SGBlackboardCategory>();
436 var selectedCategoryGuid = String.Empty;
437 for (int i = 0; i < copiedSelectionList.Count; i++)
438 {
439 var selectable = copiedSelectionList[i];
440 if (selectable is SGBlackboardCategory category)
441 {
442 selectedCategories.Add(selectable as SGBlackboardCategory);
443 }
444 }
445 if (selectedCategories.Any())
446 {
447 selectedCategoryGuid = selectedCategories[0].viewModel.associatedCategoryGuid;
448 }
449 return selectedCategoryGuid;
450 }
451
452 void InitializeViewModel(bool useDropdowns)
453 {
454 // Clear the view model
455 ViewModel.ResetViewModelData();
456 ViewModel.subtitle = BlackboardUtils.FormatPath(Model.path);
457 BlackboardShaderInputOrder propertyTypesOrder = new BlackboardShaderInputOrder();
458
459 // Property data first
460 foreach (var shaderInputType in s_ShaderInputTypes)
461 {
462 if (shaderInputType.IsAbstract)
463 continue;
464
465 var info = Attribute.GetCustomAttribute(shaderInputType, typeof(BlackboardInputInfo)) as BlackboardInputInfo;
466 string name = info?.name ?? ObjectNames.NicifyVariableName(shaderInputType.Name.Replace("ShaderProperty", ""));
467
468 // QUICK FIX TO DEAL WITH DEPRECATED COLOR PROPERTY
469 if (name.Equals("Color", StringComparison.InvariantCultureIgnoreCase) && ShaderGraphPreferences.allowDeprecatedBehaviors)
470 {
471 propertyTypesOrder.isKeyword = false;
472 propertyTypesOrder.deprecatedPropertyName = name;
473 propertyTypesOrder.version = ColorShaderProperty.deprecatedVersion;
474 ViewModel.propertyNameToAddActionMap.Add($"Color (Legacy v0)", AddShaderInputAction.AddDeprecatedPropertyAction(propertyTypesOrder));
475 ViewModel.propertyNameToAddActionMap.Add(name, AddShaderInputAction.AddPropertyAction(shaderInputType));
476 }
477 else
478 ViewModel.propertyNameToAddActionMap.Add(name, AddShaderInputAction.AddPropertyAction(shaderInputType));
479 }
480
481 // Default Keywords next
482 BlackboardShaderInputOrder keywordTypesOrder = new BlackboardShaderInputOrder();
483 keywordTypesOrder.isKeyword = true;
484 keywordTypesOrder.keywordType = KeywordType.Boolean;
485 ViewModel.defaultKeywordNameToAddActionMap.Add("Boolean", AddShaderInputAction.AddKeywordAction(keywordTypesOrder));
486 keywordTypesOrder.keywordType = KeywordType.Enum;
487 ViewModel.defaultKeywordNameToAddActionMap.Add("Enum", AddShaderInputAction.AddKeywordAction(keywordTypesOrder));
488
489 // Built-In Keywords after that
490 foreach (var builtinKeywordDescriptor in KeywordUtil.GetBuiltinKeywordDescriptors())
491 {
492 var keyword = ShaderKeyword.CreateBuiltInKeyword(builtinKeywordDescriptor);
493 // Do not allow user to add built-in keywords that conflict with user-made keywords that have the same reference name or display name
494 if (Model.keywords.Any(x => x.referenceName == keyword.referenceName || x.displayName == keyword.displayName))
495 {
496 ViewModel.disabledKeywordNameList.Add(keyword.displayName);
497 }
498 else
499 {
500 keywordTypesOrder.builtInKeyword = (ShaderKeyword)keyword.Copy();
501 ViewModel.builtInKeywordNameToAddActionMap.Add(keyword.displayName, AddShaderInputAction.AddKeywordAction(keywordTypesOrder));
502 }
503 }
504
505 if (useDropdowns)
506 {
507 BlackboardShaderInputOrder dropdownsOrder = new BlackboardShaderInputOrder();
508 dropdownsOrder.isDropdown = true;
509 ViewModel.defaultDropdownNameToAdd = new Tuple<string, IGraphDataAction>("Dropdown", AddShaderInputAction.AddDropdownAction(dropdownsOrder));
510 }
511
512 // Category data last
513 var defaultNewCategoryReference = new CategoryData("Category");
514 ViewModel.addCategoryAction = new AddCategoryAction() { categoryDataReference = defaultNewCategoryReference };
515
516 ViewModel.requestModelChangeAction = this.RequestModelChange;
517 ViewModel.categoryInfoList.AddRange(DataStore.State.categories.ToList());
518 }
519
520 internal BlackboardController(GraphData model, BlackboardViewModel inViewModel, GraphDataStore graphDataStore)
521 : base(model, inViewModel, graphDataStore)
522 {
523 // TODO: hide this more generically for category types.
524 bool useDropdowns = model.isSubGraph;
525 InitializeViewModel(useDropdowns);
526
527 blackboard = new SGBlackboard(ViewModel, this);
528
529 // Add default category at the top of the blackboard (create it if it doesn't exist already)
530 var existingDefaultCategory = DataStore.State.categories.FirstOrDefault();
531 if (existingDefaultCategory != null && existingDefaultCategory.IsNamedCategory() == false)
532 {
533 AddBlackboardCategory(graphDataStore, existingDefaultCategory);
534 }
535 else
536 {
537 // 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)
538 var uncategorizedBlackboardItems = new List<ShaderInput>();
539 foreach (var shaderProperty in DataStore.State.properties)
540 if (IsInputUncategorized(shaderProperty))
541 uncategorizedBlackboardItems.Add(shaderProperty);
542
543 foreach (var shaderKeyword in DataStore.State.keywords)
544 if (IsInputUncategorized(shaderKeyword))
545 uncategorizedBlackboardItems.Add(shaderKeyword);
546
547 if (useDropdowns)
548 {
549 foreach (var shaderDropdown in DataStore.State.dropdowns)
550 if (IsInputUncategorized(shaderDropdown))
551 uncategorizedBlackboardItems.Add(shaderDropdown);
552 }
553
554 var addCategoryAction = new AddCategoryAction();
555 addCategoryAction.categoryDataReference = CategoryData.DefaultCategory(uncategorizedBlackboardItems);
556 graphDataStore.Dispatch(addCategoryAction);
557 }
558
559 // Get the reference to default category controller after its been added
560 m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault();
561 AssertHelpers.IsNotNull(m_DefaultCategoryController, "Failed to instantiate default category.");
562
563 // Handle loaded-in categories from graph first, skipping the first/default category
564 foreach (var categoryData in ViewModel.categoryInfoList.Skip(1))
565 {
566 AddBlackboardCategory(graphDataStore, categoryData);
567 }
568 }
569
570 internal string editorPrefsBaseKey => "unity.shadergraph." + DataStore.State.objectId;
571
572 BlackboardCategoryController AddBlackboardCategory(GraphDataStore graphDataStore, CategoryData categoryInfo)
573 {
574 var blackboardCategoryViewModel = new BlackboardCategoryViewModel();
575 blackboardCategoryViewModel.parentView = blackboard;
576 blackboardCategoryViewModel.requestModelChangeAction = ViewModel.requestModelChangeAction;
577 blackboardCategoryViewModel.name = categoryInfo.name;
578 blackboardCategoryViewModel.associatedCategoryGuid = categoryInfo.categoryGuid;
579 blackboardCategoryViewModel.isExpanded = EditorPrefs.GetBool($"{editorPrefsBaseKey}.{categoryInfo.categoryGuid}.{ChangeCategoryIsExpandedAction.kEditorPrefKey}", true);
580
581 var blackboardCategoryController = new BlackboardCategoryController(categoryInfo, blackboardCategoryViewModel, graphDataStore);
582 if (m_BlackboardCategoryControllers.ContainsKey(categoryInfo.categoryGuid) == false)
583 {
584 m_BlackboardCategoryControllers.Add(categoryInfo.categoryGuid, blackboardCategoryController);
585 m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault();
586 }
587 else
588 {
589 AssertHelpers.Fail("Failed to add category controller due to category with same GUID already having been added.");
590 return null;
591 }
592 return blackboardCategoryController;
593 }
594
595 // Creates controller, view and view model for a blackboard item and adds the view to the specified index in the category
596 SGBlackboardRow InsertBlackboardRow(BlackboardItem shaderInput, int insertionIndex = -1)
597 {
598 return m_DefaultCategoryController.InsertBlackboardRow(shaderInput, insertionIndex);
599 }
600
601 public void UpdateBlackboardTitle(string newTitle)
602 {
603 ViewModel.title = newTitle;
604 blackboard.title = ViewModel.title;
605 }
606
607 protected override void RequestModelChange(IGraphDataAction changeAction)
608 {
609 DataStore.Dispatch(changeAction);
610 }
611
612 // Called by GraphDataStore.Subscribe after the model has been changed
613 protected override void ModelChanged(GraphData graphData, IGraphDataAction changeAction)
614 {
615 // Reconstruct view-model first
616 // TODO: hide this more generically for category types.
617 bool useDropdowns = graphData.isSubGraph;
618 InitializeViewModel(useDropdowns);
619
620 var graphView = ViewModel.parentView as MaterialGraphView;
621
622 switch (changeAction)
623 {
624 // If newly added input doesn't belong to any of the user-made categories, add it to the default category at top of blackboard
625 case AddShaderInputAction addBlackboardItemAction:
626 if (IsInputUncategorized(addBlackboardItemAction.shaderInputReference))
627 {
628 var blackboardRow = InsertBlackboardRow(addBlackboardItemAction.shaderInputReference);
629 if (blackboardRow != null)
630 {
631 var propertyView = blackboardRow.Q<SGBlackboardField>();
632 if (addBlackboardItemAction.addInputActionType == AddShaderInputAction.AddActionSource.AddMenu)
633 propertyView.OpenTextEditor();
634 }
635 }
636 break;
637 // Need to handle deletion of shader inputs here as opposed to BlackboardCategoryController, as currently,
638 // once removed from the categories there is no way to associate an input with the category that owns it
639 case DeleteShaderInputAction deleteShaderInputAction:
640 foreach (var shaderInput in deleteShaderInputAction.shaderInputsToDelete)
641 RemoveInputFromBlackboard(shaderInput);
642 break;
643
644 case HandleUndoRedoAction handleUndoRedoAction:
645 ClearBlackboardCategories();
646
647 foreach (var categoryData in graphData.addedCategories)
648 AddBlackboardCategory(DataStore, categoryData);
649
650 m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault();
651
652 break;
653 case CopyShaderInputAction copyShaderInputAction:
654 // 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
655 if (copyShaderInputAction.copiedShaderInput != null && IsInputUncategorized(copyShaderInputAction.copiedShaderInput))
656 {
657 var blackboardRow = InsertBlackboardRow(copyShaderInputAction.copiedShaderInput, copyShaderInputAction.insertIndex);
658 var propertyView = blackboardRow.Q<SGBlackboardField>();
659 graphView?.AddToSelectionNoUndoRecord(propertyView);
660 }
661
662 break;
663
664 case AddCategoryAction addCategoryAction:
665 AddBlackboardCategory(DataStore, addCategoryAction.categoryDataReference);
666 // Iterate through anything that is selected currently
667 foreach (var selectedElement in blackboard.selection.ToList())
668 {
669 if (selectedElement is SGBlackboardField { userData: ShaderInput shaderInput })
670 {
671 // If a blackboard item is selected, first remove it from the blackboard
672 RemoveInputFromBlackboard(shaderInput);
673
674 // Then add input to the new category
675 var addItemToCategoryAction = new AddItemToCategoryAction();
676 addItemToCategoryAction.categoryGuid = addCategoryAction.categoryDataReference.categoryGuid;
677 addItemToCategoryAction.itemToAdd = shaderInput;
678 DataStore.Dispatch(addItemToCategoryAction);
679 }
680 }
681 break;
682
683 case DeleteCategoryAction deleteCategoryAction:
684 // Clean up deleted categories
685 foreach (var categoryGUID in deleteCategoryAction.categoriesToRemoveGuids)
686 {
687 RemoveBlackboardCategory(categoryGUID);
688 }
689 break;
690
691 case MoveCategoryAction moveCategoryAction:
692 ClearBlackboardCategories();
693 foreach (var categoryData in ViewModel.categoryInfoList)
694 AddBlackboardCategory(graphData.owner.graphDataStore, categoryData);
695 break;
696
697 case CopyCategoryAction copyCategoryAction:
698 var blackboardCategory = AddBlackboardCategory(graphData.owner.graphDataStore, copyCategoryAction.newCategoryDataReference);
699 if (blackboardCategory != null)
700 graphView?.AddToSelectionNoUndoRecord(blackboardCategory.blackboardCategoryView);
701 break;
702 case ShaderVariantLimitAction shaderVariantLimitAction:
703 blackboard.SetCurrentVariantUsage(shaderVariantLimitAction.currentVariantCount, shaderVariantLimitAction.maxVariantCount);
704 break;
705 }
706
707 // Lets all event handlers this controller owns/manages know that the model has changed
708 // Usually this is to update views and make them reconstruct themself from updated view-model
709 //NotifyChange(changeAction);
710
711 // Let child controllers know about changes to this controller so they may update themselves in turn
712 //ApplyChanges();
713 }
714
715 void RemoveInputFromBlackboard(ShaderInput shaderInput)
716 {
717 // Check if input is in one of the categories
718 foreach (var controller in m_BlackboardCategoryControllers.Values)
719 {
720 var blackboardRow = controller.FindBlackboardRow(shaderInput);
721 if (blackboardRow != null)
722 {
723 controller.RemoveBlackboardRow(shaderInput);
724 return;
725 }
726 }
727 }
728
729 bool IsInputUncategorized(ShaderInput shaderInput)
730 {
731 // Skip the first category controller as that is guaranteed to be the default category
732 foreach (var categoryController in m_BlackboardCategoryControllers.Values.Skip(1))
733 {
734 if (categoryController.IsInputInCategory(shaderInput))
735 return false;
736 }
737
738 return true;
739 }
740
741 public SGBlackboardCategory GetBlackboardCategory(string inputGuid)
742 {
743 foreach (var categoryController in m_BlackboardCategoryControllers.Values)
744 {
745 if (categoryController.Model.categoryGuid == inputGuid)
746 return categoryController.blackboardCategoryView;
747 }
748
749 return null;
750 }
751
752 public SGBlackboardRow GetBlackboardRow(ShaderInput blackboardItem)
753 {
754 foreach (var categoryController in m_BlackboardCategoryControllers.Values)
755 {
756 var blackboardRow = categoryController.FindBlackboardRow(blackboardItem);
757 if (blackboardRow != null)
758 return blackboardRow;
759 }
760
761 return null;
762 }
763
764 int numberOfCategories => m_BlackboardCategoryControllers.Count;
765
766 // Gets the index after the currently selected shader input for pasting properties into this graph
767 internal int GetInsertionIndexForPaste()
768 {
769 if (blackboard?.selection == null || blackboard.selection.Count == 0)
770 {
771 return 0;
772 }
773
774 foreach (ISelectable selection in blackboard.selection)
775 {
776 if (selection is SGBlackboardField blackboardPropertyView)
777 {
778 SGBlackboardRow row = blackboardPropertyView.GetFirstAncestorOfType<SGBlackboardRow>();
779 SGBlackboardCategory category = blackboardPropertyView.GetFirstAncestorOfType<SGBlackboardCategory>();
780 if (row == null || category == null)
781 continue;
782 int blackboardFieldIndex = category.IndexOf(row);
783
784 return blackboardFieldIndex;
785 }
786 }
787
788 return 0;
789 }
790
791 void RemoveBlackboardCategory(string categoryGUID)
792 {
793 m_BlackboardCategoryControllers.TryGetValue(categoryGUID, out var blackboardCategoryController);
794 if (blackboardCategoryController != null)
795 {
796 blackboardCategoryController.Dispose();
797 m_BlackboardCategoryControllers.Remove(categoryGUID);
798 }
799 else
800 AssertHelpers.Fail("Tried to remove a category that doesn't exist. ");
801 }
802
803 public override void Dispose()
804 {
805 if (m_Blackboard == null)
806 return;
807
808 base.Dispose();
809 m_DefaultCategoryController = null;
810 ClearBlackboardCategories();
811
812 m_Blackboard?.Dispose();
813 m_Blackboard = null;
814 }
815
816 void ClearBlackboardCategories()
817 {
818 foreach (var categoryController in m_BlackboardCategoryControllers.Values)
819 {
820 categoryController.Dispose();
821 }
822 m_BlackboardCategoryControllers.Clear();
823 }
824
825 // Meant to be used by UI testing in order to clear blackboard state
826 internal void ResetBlackboardState()
827 {
828 ClearBlackboardCategories();
829 var addCategoryAction = new AddCategoryAction();
830 addCategoryAction.categoryDataReference = CategoryData.DefaultCategory();
831 DataStore.Dispatch(addCategoryAction);
832 }
833 }
834}