A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Text;
5using UnityEditor.Graphing;
6using UnityEditor.Graphing.Util;
7using UnityEngine;
8using UnityEditor.UIElements;
9using UnityEditor.Experimental.GraphView;
10using UnityEngine.UIElements;
11using UnityEditor.Searcher;
12using UnityEngine.Profiling;
13using UnityEngine.Pool;
14using Object = UnityEngine.Object;
15
16namespace UnityEditor.ShaderGraph.Drawing
17{
18 internal struct NodeEntry
19 {
20 public string[] title;
21 public AbstractMaterialNode node;
22 public int compatibleSlotId;
23 public string slotName;
24 }
25
26 class SearchWindowProvider : IDisposable
27 {
28 internal EditorWindow m_EditorWindow;
29 internal GraphData m_Graph;
30 internal GraphView m_GraphView;
31 internal Texture2D m_Icon;
32 public List<NodeEntry> currentNodeEntries;
33 public ShaderPort connectedPort { get; set; }
34 public bool nodeNeedsRepositioning { get; set; }
35 public SlotReference targetSlotReference { get; internal set; }
36 public Vector2 targetPosition { get; internal set; }
37 public VisualElement target { get; internal set; }
38 public bool regenerateEntries { get; set; }
39 private const string k_HiddenFolderName = "Hidden";
40 ShaderStageCapability m_ConnectedSlotCapability; // calculated in GenerateNodeEntries
41
42 public void Initialize(EditorWindow editorWindow, GraphData graph, GraphView graphView)
43 {
44 m_EditorWindow = editorWindow;
45 m_Graph = graph;
46 m_GraphView = graphView;
47 GenerateNodeEntries();
48
49 // Transparent icon to trick search window into indenting items
50 m_Icon = new Texture2D(1, 1);
51 m_Icon.SetPixel(0, 0, new Color(0, 0, 0, 0));
52 m_Icon.Apply();
53 }
54
55 public void Dispose()
56 {
57 if (m_Icon != null)
58 {
59 Object.DestroyImmediate(m_Icon);
60 m_Icon = null;
61 }
62
63 m_EditorWindow = null;
64 m_Graph = null;
65 m_GraphView = null;
66 connectedPort = null;
67
68 currentNodeEntries?.Clear();
69 currentNodeEntries = null;
70 }
71
72 List<int> m_Ids;
73 List<MaterialSlot> m_Slots = new List<MaterialSlot>();
74
75 public void GenerateNodeEntries()
76 {
77 Profiler.BeginSample("SearchWindowProvider.GenerateNodeEntries");
78 // First build up temporary data structure containing group & title as an array of strings (the last one is the actual title) and associated node type.
79 List<NodeEntry> nodeEntries = new List<NodeEntry>();
80
81 bool hideCustomInterpolators = m_Graph.activeTargets.All(at => at.ignoreCustomInterpolators);
82
83 if (connectedPort != null)
84 {
85 var slot = connectedPort.slot;
86
87 // Precalculate slot compatibility to avoid traversing graph for every added entry.
88 m_ConnectedSlotCapability = slot.stageCapability;
89 if (m_ConnectedSlotCapability == ShaderStageCapability.All || slot.owner is SubGraphNode)
90 {
91 m_ConnectedSlotCapability = NodeUtils.GetEffectiveShaderStageCapability(slot, true)
92 & NodeUtils.GetEffectiveShaderStageCapability(slot, false);
93 }
94 }
95 else
96 {
97 m_ConnectedSlotCapability = ShaderStageCapability.All;
98 }
99
100 if (target is ContextView contextView)
101 {
102 // Iterate all BlockFieldDescriptors currently cached on GraphData
103 foreach (var field in m_Graph.blockFieldDescriptors)
104 {
105 if (field.isHidden)
106 continue;
107
108 // Test stage
109 if (field.shaderStage != contextView.contextData.shaderStage)
110 continue;
111
112 // Create title
113 List<string> title = ListPool<string>.Get();
114 if (!string.IsNullOrEmpty(field.path))
115 {
116 var path = field.path.Split('/').ToList();
117 title.AddRange(path);
118 }
119 title.Add(field.displayName);
120
121 // Create and initialize BlockNode instance then add entry
122 var node = (BlockNode)Activator.CreateInstance(typeof(BlockNode));
123 node.Init(field);
124 AddEntries(node, title.ToArray(), nodeEntries);
125 }
126
127 SortEntries(nodeEntries);
128
129 if (contextView.contextData.shaderStage == ShaderStage.Vertex && !hideCustomInterpolators)
130 {
131 var customBlockNodeStub = (BlockNode)Activator.CreateInstance(typeof(BlockNode));
132 customBlockNodeStub.InitCustomDefault();
133 AddEntries(customBlockNodeStub, new string[] { "Custom Interpolator" }, nodeEntries);
134 }
135
136 currentNodeEntries = nodeEntries;
137 return;
138 }
139
140
141 Profiler.BeginSample("SearchWindowProvider.GenerateNodeEntries.IterateKnowNodes");
142 foreach (var type in NodeClassCache.knownNodeTypes)
143 {
144 if ((!type.IsClass || type.IsAbstract)
145 || type == typeof(PropertyNode)
146 || type == typeof(KeywordNode)
147 || type == typeof(DropdownNode)
148 || type == typeof(SubGraphNode))
149 continue;
150
151 TitleAttribute titleAttribute = NodeClassCache.GetAttributeOnNodeType<TitleAttribute>(type);
152 if (titleAttribute != null)
153 {
154 var node = (AbstractMaterialNode)Activator.CreateInstance(type);
155 if (!node.ExposeToSearcher)
156 continue;
157
158 if (ShaderGraphPreferences.allowDeprecatedBehaviors && node.latestVersion > 0)
159 {
160 var versions = node.allowedNodeVersions ?? Enumerable.Range(0, node.latestVersion + 1);
161 bool multiple = (versions.Count() > 1);
162 foreach (int i in versions)
163 {
164 var depNode = (AbstractMaterialNode)Activator.CreateInstance(type);
165 depNode.ChangeVersion(i);
166 if (multiple)
167 AddEntries(depNode, titleAttribute.title.Append($"v{i}").ToArray(), nodeEntries);
168 else
169 AddEntries(depNode, titleAttribute.title, nodeEntries);
170 }
171 }
172 else
173 {
174 AddEntries(node, titleAttribute.title, nodeEntries);
175 }
176 }
177 }
178 Profiler.EndSample();
179
180
181 Profiler.BeginSample("SearchWindowProvider.GenerateNodeEntries.IterateSubgraphAssets");
182 foreach (var asset in NodeClassCache.knownSubGraphAssets)
183 {
184 if (asset == null)
185 continue;
186
187 var node = new SubGraphNode { asset = asset };
188 var title = asset.path.Split('/').ToList();
189
190 if (asset.descendents.Contains(m_Graph.assetGuid) || asset.assetGuid == m_Graph.assetGuid)
191 {
192 continue;
193 }
194
195 if (string.IsNullOrEmpty(asset.path))
196 {
197 AddEntries(node, new string[1] { asset.name }, nodeEntries);
198 }
199 else if (title[0] != k_HiddenFolderName)
200 {
201 title.Add(asset.name);
202 AddEntries(node, title.ToArray(), nodeEntries);
203 }
204 }
205 Profiler.EndSample();
206
207
208 Profiler.BeginSample("SearchWindowProvider.GenerateNodeEntries.IterateGraphInputs");
209 foreach (var property in m_Graph.properties)
210 {
211 if (property is Serialization.MultiJsonInternal.UnknownShaderPropertyType)
212 continue;
213
214 var node = new PropertyNode();
215 node.property = property;
216 AddEntries(node, new[] { "Properties", "Property: " + property.displayName }, nodeEntries);
217 }
218 foreach (var keyword in m_Graph.keywords)
219 {
220 var node = new KeywordNode();
221 node.keyword = keyword;
222 AddEntries(node, new[] { "Keywords", "Keyword: " + keyword.displayName }, nodeEntries);
223 }
224 foreach (var dropdown in m_Graph.dropdowns)
225 {
226 var node = new DropdownNode();
227 node.dropdown = dropdown;
228 AddEntries(node, new[] { "Dropdowns", "dropdown: " + dropdown.displayName }, nodeEntries);
229 }
230 if (!hideCustomInterpolators)
231 {
232 foreach (var cibnode in m_Graph.vertexContext.blocks.Where(b => b.value.isCustomBlock))
233 {
234 var node = Activator.CreateInstance<CustomInterpolatorNode>();
235 node.ConnectToCustomBlock(cibnode.value);
236 AddEntries(node, new[] { "Custom Interpolator", cibnode.value.customName }, nodeEntries);
237 }
238 }
239 Profiler.EndSample();
240
241 SortEntries(nodeEntries);
242 currentNodeEntries = nodeEntries;
243 Profiler.EndSample();
244 }
245
246 void SortEntries(List<NodeEntry> nodeEntries)
247 {
248 // Sort the entries lexicographically by group then title with the requirement that items always comes before sub-groups in the same group.
249 // Example result:
250 // - Art/BlendMode
251 // - Art/Adjustments/ColorBalance
252 // - Art/Adjustments/Contrast
253 nodeEntries.Sort((entry1, entry2) =>
254 {
255 for (var i = 0; i < entry1.title.Length; i++)
256 {
257 if (i >= entry2.title.Length)
258 return 1;
259 var value = entry1.title[i].CompareTo(entry2.title[i]);
260 if (value != 0)
261 {
262 // Make sure that leaves go before nodes
263 if (entry1.title.Length != entry2.title.Length && (i == entry1.title.Length - 1 || i == entry2.title.Length - 1))
264 {
265 //once nodes are sorted, sort slot entries by slot order instead of alphebetically
266 var alphaOrder = entry1.title.Length < entry2.title.Length ? -1 : 1;
267 var slotOrder = entry1.compatibleSlotId.CompareTo(entry2.compatibleSlotId);
268 return alphaOrder.CompareTo(slotOrder);
269 }
270
271 return value;
272 }
273 }
274 return 0;
275 });
276 }
277
278 void AddEntries(AbstractMaterialNode node, string[] title, List<NodeEntry> addNodeEntries)
279 {
280 if (m_Graph.isSubGraph && !node.allowedInSubGraph)
281 return;
282 if (!m_Graph.isSubGraph && !node.allowedInMainGraph)
283 return;
284 if (connectedPort == null)
285 {
286 addNodeEntries.Add(new NodeEntry
287 {
288 node = node,
289 title = title,
290 compatibleSlotId = -1
291 });
292 return;
293 }
294
295 var connectedSlot = connectedPort.slot;
296 m_Slots.Clear();
297 node.GetSlots(m_Slots);
298
299 foreach (var slot in m_Slots)
300 {
301 if (!slot.IsCompatibleWith(connectedSlot))
302 {
303 continue;
304 }
305
306 if (!slot.IsCompatibleStageWith(m_ConnectedSlotCapability))
307 {
308 continue;
309 }
310
311 //var entryTitle = new string[title.Length];
312 //title.CopyTo(entryTitle, 0);
313 //entryTitle[entryTitle.Length - 1] += ": " + slot.displayName;
314 addNodeEntries.Add(new NodeEntry
315 {
316 title = title,
317 node = node,
318 compatibleSlotId = slot.id,
319 slotName = slot.displayName
320 });
321 }
322 }
323 }
324 class SearcherProvider : SearchWindowProvider
325 {
326 public Searcher.Searcher LoadSearchWindow()
327 {
328 if (regenerateEntries)
329 {
330 GenerateNodeEntries();
331 regenerateEntries = false;
332 }
333
334 //create empty root for searcher tree
335 var root = new List<SearcherItem>();
336 var dummyEntry = new NodeEntry();
337
338 foreach (var nodeEntry in currentNodeEntries)
339 {
340 SearcherItem item = null;
341 SearcherItem parent = null;
342 for (int i = 0; i < nodeEntry.title.Length; i++)
343 {
344 var pathEntry = nodeEntry.title[i];
345 List<SearcherItem> children = parent != null ? parent.Children : root;
346 item = children.Find(x => x.Name == pathEntry);
347
348 if (item == null)
349 {
350 //if we have slot entries and are at a leaf, add the slot name to the entry title
351 if (nodeEntry.compatibleSlotId != -1 && i == nodeEntry.title.Length - 1)
352 item = new SearchNodeItem(pathEntry + ": " + nodeEntry.slotName, nodeEntry, nodeEntry.node.synonyms);
353 //if we don't have slot entries and are at a leaf, add userdata to the entry
354 else if (nodeEntry.compatibleSlotId == -1 && i == nodeEntry.title.Length - 1)
355 item = new SearchNodeItem(pathEntry, nodeEntry, nodeEntry.node.synonyms);
356 //if we aren't a leaf, don't add user data
357 else
358 item = new SearchNodeItem(pathEntry, dummyEntry, null);
359
360 if (parent != null)
361 {
362 parent.AddChild(item);
363 }
364 else
365 {
366 children.Add(item);
367 }
368 }
369
370 parent = item;
371
372 if (parent.Depth == 0 && !root.Contains(parent))
373 root.Add(parent);
374 }
375 }
376
377 var nodeDatabase = SearcherDatabase.Create(root, string.Empty, false);
378
379 return new Searcher.Searcher(nodeDatabase, new SearchWindowAdapter("Create Node"));
380 }
381
382 public bool OnSearcherSelectEntry(SearcherItem entry, Vector2 screenMousePosition)
383 {
384 if (entry == null || (entry as SearchNodeItem).NodeGUID.node == null)
385 return true;
386
387 var nodeEntry = (entry as SearchNodeItem).NodeGUID;
388
389 if (nodeEntry.node is PropertyNode propNode)
390 if (propNode.property is Serialization.MultiJsonInternal.UnknownShaderPropertyType)
391 return true;
392
393 var node = CopyNodeForGraph(nodeEntry.node);
394
395 var windowRoot = m_EditorWindow.rootVisualElement;
396 var windowMousePosition = windowRoot.ChangeCoordinatesTo(windowRoot.parent, screenMousePosition); //- m_EditorWindow.position.position);
397 var graphMousePosition = m_GraphView.contentViewContainer.WorldToLocal(windowMousePosition);
398
399 m_Graph.owner.RegisterCompleteObjectUndo("Add " + node.name);
400
401 if (node is BlockNode blockNode)
402 {
403 if (!(target is ContextView contextView))
404 return true;
405
406 // ensure custom blocks have a unique name provided the existing context.
407 if (blockNode.isCustomBlock)
408 {
409 HashSet<string> usedNames = new HashSet<string>();
410 foreach (var other in contextView.contextData.blocks) usedNames.Add(other.value.descriptor.displayName);
411 blockNode.customName = GraphUtil.SanitizeName(usedNames, "{0}_{1}", blockNode.descriptor.displayName);
412 }
413 // Test against all current BlockNodes in the Context
414 // Never allow duplicate BlockNodes
415 else if (contextView.contextData.blocks.Where(x => x.value.name == blockNode.name).FirstOrDefault().value != null)
416 {
417 return true;
418 }
419
420 // Insert block to Data
421 blockNode.owner = m_Graph;
422 int index = contextView.GetInsertionIndex(screenMousePosition);
423 m_Graph.AddBlock(blockNode, contextView.contextData, index);
424 return true;
425 }
426
427 var drawState = node.drawState;
428 drawState.position = new Rect(graphMousePosition, Vector2.zero);
429 node.drawState = drawState;
430 m_Graph.AddNode(node);
431
432 if (connectedPort != null)
433 {
434 var connectedSlot = connectedPort.slot;
435 var connectedSlotReference = connectedSlot.owner.GetSlotReference(connectedSlot.id);
436 var compatibleSlotReference = node.GetSlotReference(nodeEntry.compatibleSlotId);
437
438 var fromReference = connectedSlot.isOutputSlot ? connectedSlotReference : compatibleSlotReference;
439 var toReference = connectedSlot.isOutputSlot ? compatibleSlotReference : connectedSlotReference;
440 m_Graph.Connect(fromReference, toReference);
441
442 nodeNeedsRepositioning = true;
443 targetSlotReference = compatibleSlotReference;
444 targetPosition = graphMousePosition;
445 }
446
447 return true;
448 }
449
450 public AbstractMaterialNode CopyNodeForGraph(AbstractMaterialNode oldNode)
451 {
452 var newNode = (AbstractMaterialNode)Activator.CreateInstance(oldNode.GetType());
453 if (ShaderGraphPreferences.allowDeprecatedBehaviors && oldNode.sgVersion != newNode.sgVersion)
454 {
455 newNode.ChangeVersion(oldNode.sgVersion);
456 }
457 if (newNode is SubGraphNode subgraphNode)
458 {
459 subgraphNode.asset = ((SubGraphNode)oldNode).asset;
460 }
461 else if (newNode is PropertyNode propertyNode)
462 {
463 propertyNode.owner = m_Graph;
464 propertyNode.property = ((PropertyNode)oldNode).property;
465 propertyNode.owner = null;
466 }
467 else if (newNode is KeywordNode keywordNode)
468 {
469 keywordNode.owner = m_Graph;
470 keywordNode.keyword = ((KeywordNode)oldNode).keyword;
471 keywordNode.owner = null;
472 }
473 else if (newNode is DropdownNode dropdownNode)
474 {
475 dropdownNode.owner = m_Graph;
476 dropdownNode.dropdown = ((DropdownNode)oldNode).dropdown;
477 dropdownNode.owner = null;
478 }
479 else if (newNode is BlockNode blockNode)
480 {
481 blockNode.owner = m_Graph;
482 blockNode.Init(((BlockNode)oldNode).descriptor);
483 blockNode.owner = null;
484 }
485 else if (newNode is CustomInterpolatorNode cinode)
486 {
487 cinode.owner = m_Graph;
488 cinode.ConnectToCustomBlockByName(((CustomInterpolatorNode)oldNode).customBlockNodeName);
489 cinode.owner = null;
490 }
491 return newNode;
492 }
493 }
494}