A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Reflection;
5using UnityEditor.Graphing.Util;
6using UnityEngine;
7using UnityEditor.Graphing;
8using Object = UnityEngine.Object;
9using UnityEditor.Experimental.GraphView;
10using UnityEditor.ShaderGraph.Drawing.Inspector.PropertyDrawers;
11using UnityEditor.ShaderGraph.Drawing.Views;
12using UnityEditor.ShaderGraph.Internal;
13using UnityEditor.ShaderGraph.Serialization;
14using UnityEngine.UIElements;
15using Edge = UnityEditor.Experimental.GraphView.Edge;
16using Node = UnityEditor.Experimental.GraphView.Node;
17using UnityEngine.Pool;
18
19namespace UnityEditor.ShaderGraph.Drawing
20{
21 sealed class MaterialGraphView : GraphView, IInspectable, ISelectionProvider
22 {
23 readonly MethodInfo m_UndoRedoPerformedMethodInfo;
24
25 public MaterialGraphView()
26 {
27 styleSheets.Add(Resources.Load<StyleSheet>("Styles/MaterialGraphView"));
28 serializeGraphElements = SerializeGraphElementsImplementation;
29 canPasteSerializedData = CanPasteSerializedDataImplementation;
30 unserializeAndPaste = UnserializeAndPasteImplementation;
31 deleteSelection = DeleteSelectionImplementation;
32 elementsInsertedToStackNode = ElementsInsertedToStackNode;
33 RegisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent);
34 RegisterCallback<DragPerformEvent>(OnDragPerformEvent);
35 RegisterCallback<MouseMoveEvent>(OnMouseMoveEvent);
36
37 this.viewTransformChanged += OnTransformChanged;
38
39 // Get reference to GraphView assembly
40 Assembly graphViewAssembly = null;
41 foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
42 {
43 var assemblyName = assembly.GetName().ToString();
44 if (assemblyName.Contains("GraphView"))
45 {
46 graphViewAssembly = assembly;
47 }
48 }
49
50 Type graphViewType = graphViewAssembly?.GetType("UnityEditor.Experimental.GraphView.GraphView");
51 // Cache the method info for this function to be used through application lifetime
52 m_UndoRedoPerformedMethodInfo = graphViewType?.GetMethod("UndoRedoPerformed",
53 BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic,
54 null,
55 new Type[] { typeof(UndoRedoInfo).MakeByRefType()},
56 null);
57 }
58
59 // GraphView has a bug where the viewTransform will be reset to default when swapping between two
60 // GraphViewEditor windows of the same type. This is a hack to prevent that from happening w/as little
61 // halo as possible.
62 Vector3 lkgPosition;
63 Vector3 lkgScale;
64 void OnTransformChanged(GraphView graphView)
65 {
66 if (!graphView.viewTransform.position.Equals(Vector3.zero))
67 {
68 lkgPosition = graphView.viewTransform.position;
69 lkgScale = graphView.viewTransform.scale;
70 }
71 else if (!lkgPosition.Equals(Vector3.zero))
72 {
73 graphView.UpdateViewTransform(lkgPosition, lkgScale);
74 }
75 }
76
77 protected internal override bool canCutSelection
78 {
79 get { return selection.OfType<IShaderNodeView>().Any(x => x.node.canCutNode) || selection.OfType<Group>().Any() || selection.OfType<SGBlackboardField>().Any() || selection.OfType<SGBlackboardCategory>().Any() || selection.OfType<StickyNote>().Any(); }
80 }
81
82 protected internal override bool canCopySelection
83 {
84 get { return selection.OfType<IShaderNodeView>().Any(x => x.node.canCopyNode) || selection.OfType<Group>().Any() || selection.OfType<SGBlackboardField>().Any() || selection.OfType<SGBlackboardCategory>().Any() || selection.OfType<StickyNote>().Any(); }
85 }
86
87 public MaterialGraphView(GraphData graph, Action previewUpdateDelegate) : this()
88 {
89 this.graph = graph;
90 this.m_PreviewManagerUpdateDelegate = previewUpdateDelegate;
91 }
92
93 [Inspectable("GraphData", null)]
94 public GraphData graph { get; private set; }
95
96 Action m_BlackboardFieldDropDelegate;
97 internal Action blackboardFieldDropDelegate
98 {
99 get => m_BlackboardFieldDropDelegate;
100 set => m_BlackboardFieldDropDelegate = value;
101 }
102
103 public List<ISelectable> GetSelection => selection;
104
105 Action m_InspectorUpdateDelegate;
106 Action m_PreviewManagerUpdateDelegate;
107
108 public string inspectorTitle => this.graph.path;
109
110 public object GetObjectToInspect()
111 {
112 return graph;
113 }
114
115 public void SupplyDataToPropertyDrawer(IPropertyDrawer propertyDrawer, Action inspectorUpdateDelegate)
116 {
117 m_InspectorUpdateDelegate = inspectorUpdateDelegate;
118 if (propertyDrawer is GraphDataPropertyDrawer graphDataPropertyDrawer)
119 {
120 graphDataPropertyDrawer.GetPropertyData(this.ChangeTargetSettings, ChangePrecision);
121 }
122 }
123
124 void ChangeTargetSettings()
125 {
126 var activeBlocks = graph.GetActiveBlocksForAllActiveTargets();
127 if (ShaderGraphPreferences.autoAddRemoveBlocks)
128 {
129 graph.AddRemoveBlocksFromActiveList(activeBlocks);
130 }
131
132 graph.UpdateActiveBlocks(activeBlocks);
133 this.m_PreviewManagerUpdateDelegate();
134 this.m_InspectorUpdateDelegate();
135 }
136
137 void ChangePrecision(GraphPrecision newGraphDefaultPrecision)
138 {
139 if (graph.graphDefaultPrecision == newGraphDefaultPrecision)
140 return;
141
142 graph.owner.RegisterCompleteObjectUndo("Change Graph Default Precision");
143
144 graph.SetGraphDefaultPrecision(newGraphDefaultPrecision);
145
146 var graphEditorView = this.GetFirstAncestorOfType<GraphEditorView>();
147 if (graphEditorView == null)
148 return;
149
150 var nodeList = this.Query<MaterialNodeView>().ToList();
151 graphEditorView.colorManager.SetNodesDirty(nodeList);
152
153 graph.ValidateGraph();
154 graphEditorView.colorManager.UpdateNodeViews(nodeList);
155 foreach (var node in graph.GetNodes<AbstractMaterialNode>())
156 {
157 node.Dirty(ModificationScope.Graph);
158 }
159 }
160
161 public Action onConvertToSubgraphClick { get; set; }
162 public Vector2 cachedMousePosition { get; private set; }
163
164 public bool wasUndoRedoPerformed { get; set; }
165
166 // GraphView has UQueryState<Node> nodes built in to query for Nodes
167 // We need this for Contexts but we might as well cast it to a list once
168 public List<ContextView> contexts { get; set; }
169
170 // We have to manually update Contexts
171 // Currently only called during GraphEditorView ctor as our Contexts are static
172 public void UpdateContextList()
173 {
174 var contextQuery = contentViewContainer.Query<ContextView>().Build();
175 contexts = contextQuery.ToList();
176 }
177
178 // We need a way to access specific ContextViews
179 public ContextView GetContext(ContextData contextData)
180 {
181 return contexts.FirstOrDefault(s => s.contextData == contextData);
182 }
183
184 public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
185 {
186 var compatibleAnchors = new List<Port>();
187 var startSlot = startAnchor.GetSlot();
188 if (startSlot == null)
189 return compatibleAnchors;
190
191 var startStage = startSlot.stageCapability;
192 // If this is a sub-graph node we always have to check the effective stage as we might have to trace back through the sub-graph
193 if (startStage == ShaderStageCapability.All || startSlot.owner is SubGraphNode)
194 startStage = NodeUtils.GetEffectiveShaderStageCapability(startSlot, true) & NodeUtils.GetEffectiveShaderStageCapability(startSlot, false);
195
196 foreach (var candidateAnchor in ports.ToList())
197 {
198 var candidateSlot = candidateAnchor.GetSlot();
199
200 if (!startSlot.IsCompatibleWith(candidateSlot))
201 continue;
202
203 if (startStage != ShaderStageCapability.All)
204 {
205 var candidateStage = candidateSlot.stageCapability;
206 if (candidateStage == ShaderStageCapability.All || candidateSlot.owner is SubGraphNode)
207 candidateStage = NodeUtils.GetEffectiveShaderStageCapability(candidateSlot, true)
208 & NodeUtils.GetEffectiveShaderStageCapability(candidateSlot, false);
209 if (candidateStage != ShaderStageCapability.All && candidateStage != startStage)
210 continue;
211
212 // None stage can only connect to All stage, otherwise you can connect invalid connections
213 if (startStage == ShaderStageCapability.None && candidateStage != ShaderStageCapability.All)
214 continue;
215 }
216
217 compatibleAnchors.Add(candidateAnchor);
218 }
219 return compatibleAnchors;
220 }
221
222 internal bool ResetSelectedBlockNodes()
223 {
224 bool anyNodesWereReset = false;
225 var selectedBlocknodes = selection.FindAll(e => e is MaterialNodeView && ((MaterialNodeView)e).node is BlockNode).Cast<MaterialNodeView>().ToArray();
226 foreach (var mNode in selectedBlocknodes)
227 {
228 var bNode = mNode.node as BlockNode;
229 var context = GetContext(bNode.contextData);
230
231 // Check if the node is currently floating (it's parent isn't the context view that owns it).
232 // If the node's not floating then the block doesn't need to be reset.
233 bool isFloating = mNode.parent != context;
234 if (!isFloating)
235 continue;
236
237 anyNodesWereReset = true;
238 RemoveElement(mNode);
239 context.InsertBlock(mNode);
240
241 // TODO: StackNode in GraphView (Trunk) has no interface to reset drop previews. The least intrusive
242 // solution is to call its DragLeave until its interface can be improved.
243 context.DragLeave(null, null, null, null);
244 }
245 if (selectedBlocknodes.Length > 0)
246 graph.ValidateCustomBlockLimit();
247 return anyNodesWereReset;
248 }
249
250 public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
251 {
252 Vector2 mousePosition = evt.mousePosition;
253
254 // If the target wasn't a block node, but there is one selected (and reset) by the time we reach this point,
255 // it means a block node was in an invalid configuration and that it may be unsafe to build the context menu.
256 bool targetIsBlockNode = evt.target is MaterialNodeView && ((MaterialNodeView)evt.target).node is BlockNode;
257 if (ResetSelectedBlockNodes() && !targetIsBlockNode)
258 {
259 return;
260 }
261
262 base.BuildContextualMenu(evt);
263 if (evt.target is GraphView)
264 {
265 evt.menu.InsertAction(1, "Create Sticky Note", (e) => { AddStickyNote(mousePosition); });
266
267 foreach (AbstractMaterialNode node in graph.GetNodes<AbstractMaterialNode>())
268 {
269 var keyHint = ShaderGraphShortcuts.GetKeycodeForContextMenu(ShaderGraphShortcuts.nodePreviewShortcutID);
270 if (node.hasPreview && node.previewExpanded == true)
271 evt.menu.InsertAction(2, $"Collapse All Previews {keyHint}", CollapsePreviews, (a) => DropdownMenuAction.Status.Normal);
272 if (node.hasPreview && node.previewExpanded == false)
273 evt.menu.InsertAction(2, $"Expand All Previews {keyHint}", ExpandPreviews, (a) => DropdownMenuAction.Status.Normal);
274 }
275 evt.menu.AppendSeparator();
276 }
277
278 if (evt.target is GraphView || evt.target is Node)
279 {
280 if (evt.target is Node node)
281 {
282 if (!selection.Contains(node))
283 {
284 selection.Clear();
285 selection.Add(node);
286 }
287 }
288
289 evt.menu.AppendAction("Select/Unused Nodes", SelectUnusedNodes);
290
291 InitializeViewSubMenu(evt);
292 InitializePrecisionSubMenu(evt);
293
294 evt.menu.AppendAction("Convert To/Sub-graph", ConvertToSubgraph, ConvertToSubgraphStatus);
295 evt.menu.AppendAction("Convert To/Inline Node", ConvertToInlineNode, ConvertToInlineNodeStatus);
296 evt.menu.AppendAction("Convert To/Property", ConvertToProperty, ConvertToPropertyStatus);
297 evt.menu.AppendSeparator();
298
299 var editorView = GetFirstAncestorOfType<GraphEditorView>();
300 if (editorView.colorManager.activeSupportsCustom && selection.OfType<MaterialNodeView>().Any())
301 {
302 evt.menu.AppendSeparator();
303 evt.menu.AppendAction("Color/Change...", ChangeCustomNodeColor,
304 eventBase => DropdownMenuAction.Status.Normal);
305
306 evt.menu.AppendAction("Color/Reset", menuAction =>
307 {
308 graph.owner.RegisterCompleteObjectUndo("Reset Node Color");
309 foreach (var selectable in selection)
310 {
311 if (selectable is MaterialNodeView nodeView)
312 {
313 nodeView.node.ResetColor(editorView.colorManager.activeProviderName);
314 editorView.colorManager.UpdateNodeView(nodeView);
315 }
316 }
317 }, eventBase => DropdownMenuAction.Status.Normal);
318 }
319
320 if (selection.OfType<IShaderNodeView>().Count() == 1)
321 {
322 evt.menu.AppendSeparator();
323 var sc = ShaderGraphShortcuts.GetKeycodeForContextMenu(ShaderGraphShortcuts.summonDocumentationShortcutID);
324 evt.menu.AppendAction($"Open Documentation {sc}", SeeDocumentation, SeeDocumentationStatus);
325 }
326 if (selection.OfType<IShaderNodeView>().Count() == 1 && selection.OfType<IShaderNodeView>().First().node is SubGraphNode)
327 {
328 evt.menu.AppendSeparator();
329 evt.menu.AppendAction("Open Sub Graph", OpenSubGraph, (a) => DropdownMenuAction.Status.Normal);
330 }
331 }
332 evt.menu.AppendSeparator();
333 if (evt.target is StickyNote)
334 {
335 evt.menu.AppendAction("Select/Unused Nodes", SelectUnusedNodes);
336 evt.menu.AppendSeparator();
337 }
338
339 // This needs to work on nodes, groups and properties
340 if ((evt.target is Node) || (evt.target is StickyNote))
341 {
342 var scg = ShaderGraphShortcuts.GetKeycodeForContextMenu(ShaderGraphShortcuts.nodeGroupShortcutID);
343 evt.menu.AppendAction($"Group Selection {scg}", _ => GroupSelection(), (a) =>
344 {
345 List<ISelectable> filteredSelection = new List<ISelectable>();
346
347 foreach (ISelectable selectedObject in selection)
348 {
349 if (selectedObject is Group)
350 return DropdownMenuAction.Status.Disabled;
351 GraphElement ge = selectedObject as GraphElement;
352 if (ge.userData is BlockNode)
353 {
354 return DropdownMenuAction.Status.Disabled;
355 }
356 if (ge.userData is IGroupItem)
357 {
358 filteredSelection.Add(ge);
359 }
360 }
361
362 if (filteredSelection.Count > 0)
363 return DropdownMenuAction.Status.Normal;
364
365 return DropdownMenuAction.Status.Disabled;
366 });
367
368 var scu = ShaderGraphShortcuts.GetKeycodeForContextMenu(ShaderGraphShortcuts.nodeUnGroupShortcutID);
369 evt.menu.AppendAction($"Ungroup Selection {scu}", _ => RemoveFromGroupNode(), (a) =>
370 {
371 List<ISelectable> filteredSelection = new List<ISelectable>();
372
373 foreach (ISelectable selectedObject in selection)
374 {
375 if (selectedObject is Group)
376 return DropdownMenuAction.Status.Disabled;
377 GraphElement ge = selectedObject as GraphElement;
378 if (ge.userData is IGroupItem)
379 {
380 if (ge.GetContainingScope() is Group)
381 filteredSelection.Add(ge);
382 }
383 }
384
385 if (filteredSelection.Count > 0)
386 return DropdownMenuAction.Status.Normal;
387
388 return DropdownMenuAction.Status.Disabled;
389 });
390 }
391
392 if (evt.target is ShaderGroup shaderGroup)
393 {
394 evt.menu.AppendAction("Select/Unused Nodes", SelectUnusedNodes);
395 evt.menu.AppendSeparator();
396 if (!selection.Contains(shaderGroup))
397 {
398 selection.Add(shaderGroup);
399 }
400
401 var data = shaderGroup.userData;
402 int count = evt.menu.MenuItems().Count;
403 evt.menu.InsertAction(count, "Delete Group and Contents", (e) => RemoveNodesInsideGroup(e, data), DropdownMenuAction.AlwaysEnabled);
404 }
405
406 if (evt.target is SGBlackboardField || evt.target is SGBlackboardCategory)
407 {
408 evt.menu.AppendAction("Delete", (e) => DeleteSelectionImplementation("Delete", AskUser.DontAskUser), (e) => canDeleteSelection ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
409 evt.menu.AppendAction("Duplicate %d", (e) => DuplicateSelection(), (a) => canDuplicateSelection ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
410 }
411
412 // Sticky notes aren't given these context menus in GraphView because it checks for specific types.
413 // We can manually add them back in here (although the context menu ordering is different).
414 if (evt.target is StickyNote)
415 {
416 evt.menu.AppendAction("Copy %d", (e) => CopySelectionCallback(), (a) => canCopySelection ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
417 evt.menu.AppendAction("Cut %d", (e) => CutSelectionCallback(), (a) => canCutSelection ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
418 evt.menu.AppendAction("Duplicate %d", (e) => DuplicateSelectionCallback(), (a) => canDuplicateSelection ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
419 }
420
421 // Contextual menu
422 if (evt.target is Edge)
423 {
424 var target = evt.target as Edge;
425 var pos = evt.mousePosition;
426
427 var keyHint = ShaderGraphShortcuts.GetKeycodeForContextMenu(ShaderGraphShortcuts.createRedirectNodeShortcutID);
428 evt.menu.AppendSeparator();
429 evt.menu.AppendAction($"Add Redirect Node {keyHint}", e => CreateRedirectNode(pos, target));
430 }
431 }
432
433 public void CreateRedirectNode(Vector2 position, Edge edgeTarget)
434 {
435 var outputSlot = edgeTarget.output.GetSlot();
436 var inputSlot = edgeTarget.input.GetSlot();
437 // Need to check if the Nodes that are connected are in a group or not
438 // If they are in the same group we also add in the Redirect Node
439 // var groupGuidOutputNode = graph.GetNodeFromGuid(outputSlot.slotReference.nodeGuid).groupGuid;
440 // var groupGuidInputNode = graph.GetNodeFromGuid(inputSlot.slotReference.nodeGuid).groupGuid;
441 GroupData group = null;
442 if (outputSlot.owner.group == inputSlot.owner.group)
443 {
444 group = inputSlot.owner.group;
445 }
446
447 RedirectNodeData.Create(graph, outputSlot.concreteValueType, contentViewContainer.WorldToLocal(position), inputSlot.slotReference,
448 outputSlot.slotReference, group);
449 }
450
451 void SelectUnusedNodes(DropdownMenuAction action)
452 {
453 graph.owner.RegisterCompleteObjectUndo("Select Unused Nodes");
454 ClearSelection();
455
456 List<AbstractMaterialNode> endNodes = new List<AbstractMaterialNode>();
457 if (!graph.isSubGraph)
458 {
459 var nodeView = graph.GetNodes<BlockNode>();
460 foreach (BlockNode blockNode in nodeView)
461 {
462 endNodes.Add(blockNode as AbstractMaterialNode);
463 }
464 }
465 else
466 {
467 var nodes = graph.GetNodes<SubGraphOutputNode>();
468 foreach (var node in nodes)
469 {
470 endNodes.Add(node);
471 }
472 }
473
474 var nodesConnectedToAMasterNode = new HashSet<AbstractMaterialNode>();
475
476 // Get the list of nodes from Master nodes or SubGraphOutputNode
477 foreach (var abs in endNodes)
478 {
479 NodeUtils.DepthFirstCollectNodesFromNode(nodesConnectedToAMasterNode, abs);
480 }
481
482 selection.Clear();
483 // Get all nodes and then compare with the master nodes list
484 var allNodes = nodes.ToList().OfType<IShaderNodeView>();
485 foreach (IShaderNodeView materialNodeView in allNodes)
486 {
487 if (!nodesConnectedToAMasterNode.Contains(materialNodeView.node))
488 {
489 var nd = materialNodeView as GraphElement;
490 AddToSelection(nd);
491 }
492 }
493 }
494
495 public delegate void SelectionChanged(List<ISelectable> selection);
496 public SelectionChanged OnSelectionChange;
497 public override void AddToSelection(ISelectable selectable)
498 {
499 base.AddToSelection(selectable);
500
501 OnSelectionChange?.Invoke(selection);
502 }
503
504 // Replicating these private GraphView functions as we need them for our own purposes
505 internal void AddToSelectionNoUndoRecord(GraphElement graphElement)
506 {
507 graphElement.selected = true;
508 selection.Add(graphElement);
509 graphElement.OnSelected();
510
511 OnSelectionChange?.Invoke(selection);
512
513 // To ensure that the selected GraphElement gets unselected if it is removed from the GraphView.
514 graphElement.RegisterCallback<DetachFromPanelEvent>(OnSelectedElementDetachedFromPanel);
515
516 graphElement.MarkDirtyRepaint();
517 }
518
519 public override void RemoveFromSelection(ISelectable selectable)
520 {
521 base.RemoveFromSelection(selectable);
522
523 if (OnSelectionChange != null)
524 OnSelectionChange(selection);
525 }
526
527 internal void RemoveFromSelectionNoUndoRecord(ISelectable selectable)
528 {
529 var graphElement = selectable as GraphElement;
530 if (graphElement == null)
531 return;
532 graphElement.selected = false;
533
534 OnSelectionChange?.Invoke(selection);
535
536 selection.Remove(selectable);
537 graphElement.OnUnselected();
538 graphElement.UnregisterCallback<DetachFromPanelEvent>(OnSelectedElementDetachedFromPanel);
539 graphElement.MarkDirtyRepaint();
540 }
541
542 private void OnSelectedElementDetachedFromPanel(DetachFromPanelEvent evt)
543 {
544 RemoveFromSelectionNoUndoRecord(evt.target as ISelectable);
545 }
546
547 public override void ClearSelection()
548 {
549 base.ClearSelection();
550
551 OnSelectionChange?.Invoke(selection);
552 }
553
554 internal bool ClearSelectionNoUndoRecord()
555 {
556 foreach (var graphElement in selection.OfType<GraphElement>())
557 {
558 graphElement.selected = false;
559 graphElement.OnUnselected();
560 graphElement.UnregisterCallback<DetachFromPanelEvent>(OnSelectedElementDetachedFromPanel);
561 graphElement.MarkDirtyRepaint();
562 }
563
564 OnSelectionChange?.Invoke(selection);
565
566 bool selectionWasNotEmpty = selection.Any();
567 selection.Clear();
568
569 return selectionWasNotEmpty;
570 }
571
572 private void RemoveNodesInsideGroup(DropdownMenuAction action, GroupData data)
573 {
574 graph.owner.RegisterCompleteObjectUndo("Delete Group and Contents");
575 var groupItems = graph.GetItemsInGroup(data);
576 graph.RemoveElements(groupItems.OfType<AbstractMaterialNode>().ToArray(), new IEdge[] { }, new[] { data }, groupItems.OfType<StickyNoteData>().ToArray());
577 }
578
579 private void InitializePrecisionSubMenu(ContextualMenuPopulateEvent evt)
580 {
581 // Default the menu buttons to disabled
582 DropdownMenuAction.Status inheritPrecisionAction = DropdownMenuAction.Status.Disabled;
583 DropdownMenuAction.Status floatPrecisionAction = DropdownMenuAction.Status.Disabled;
584 DropdownMenuAction.Status halfPrecisionAction = DropdownMenuAction.Status.Disabled;
585
586 // Check which precisions are available to switch to
587 foreach (MaterialNodeView selectedNode in selection.Where(x => x is MaterialNodeView).Select(x => x as MaterialNodeView))
588 {
589 if (selectedNode.node.precision != Precision.Inherit)
590 inheritPrecisionAction = DropdownMenuAction.Status.Normal;
591 if (selectedNode.node.precision != Precision.Single)
592 floatPrecisionAction = DropdownMenuAction.Status.Normal;
593 if (selectedNode.node.precision != Precision.Half)
594 halfPrecisionAction = DropdownMenuAction.Status.Normal;
595 }
596
597 // Create the menu options
598 evt.menu.AppendAction("Precision/Inherit", _ => SetNodePrecisionOnSelection(Precision.Inherit), (a) => inheritPrecisionAction);
599 evt.menu.AppendAction("Precision/Single", _ => SetNodePrecisionOnSelection(Precision.Single), (a) => floatPrecisionAction);
600 evt.menu.AppendAction("Precision/Half", _ => SetNodePrecisionOnSelection(Precision.Half), (a) => halfPrecisionAction);
601 }
602
603 private void InitializeViewSubMenu(ContextualMenuPopulateEvent evt)
604 {
605 // Default the menu buttons to disabled
606 DropdownMenuAction.Status expandPreviewAction = DropdownMenuAction.Status.Disabled;
607 DropdownMenuAction.Status collapsePreviewAction = DropdownMenuAction.Status.Disabled;
608 DropdownMenuAction.Status minimizeAction = DropdownMenuAction.Status.Disabled;
609 DropdownMenuAction.Status maximizeAction = DropdownMenuAction.Status.Disabled;
610
611 // Initialize strings
612 var previewKeyHint = ShaderGraphShortcuts.GetKeycodeForContextMenu(ShaderGraphShortcuts.nodePreviewShortcutID);
613 var portKeyHint = ShaderGraphShortcuts.GetKeycodeForContextMenu(ShaderGraphShortcuts.nodeCollapsedShortcutID);
614
615 string expandPreviewText = $"View/Expand Previews {previewKeyHint}";
616 string collapsePreviewText = $"View/Collapse Previews {previewKeyHint}";
617 string expandPortText = $"View/Expand Ports {portKeyHint}";
618 string collapsePortText = $"View/Collapse Ports {portKeyHint}";
619 if (selection.Count == 1)
620 {
621 collapsePreviewText = $"View/Collapse Preview {previewKeyHint}";
622 expandPreviewText = $"View/Expand Preview {previewKeyHint}";
623 }
624
625 // Check if we can expand or collapse the ports/previews
626 foreach (MaterialNodeView selectedNode in selection.Where(x => x is MaterialNodeView).Select(x => x as MaterialNodeView))
627 {
628 if (selectedNode.node.hasPreview)
629 {
630 if (selectedNode.node.previewExpanded)
631 collapsePreviewAction = DropdownMenuAction.Status.Normal;
632 else
633 expandPreviewAction = DropdownMenuAction.Status.Normal;
634 }
635
636 if (selectedNode.CanToggleNodeExpanded())
637 {
638 if (selectedNode.expanded)
639 minimizeAction = DropdownMenuAction.Status.Normal;
640 else
641 maximizeAction = DropdownMenuAction.Status.Normal;
642 }
643 }
644
645 // Create the menu options
646 evt.menu.AppendAction(collapsePortText, _ => SetNodeExpandedForSelectedNodes(false), (a) => minimizeAction);
647 evt.menu.AppendAction(expandPortText, _ => SetNodeExpandedForSelectedNodes(true), (a) => maximizeAction);
648
649 evt.menu.AppendSeparator("View/");
650
651 evt.menu.AppendAction(expandPreviewText, _ => SetPreviewExpandedForSelectedNodes(true), (a) => expandPreviewAction);
652 evt.menu.AppendAction(collapsePreviewText, _ => SetPreviewExpandedForSelectedNodes(false), (a) => collapsePreviewAction);
653 }
654
655 void ChangeCustomNodeColor(DropdownMenuAction menuAction)
656 {
657 // Color Picker is internal :(
658 var t = typeof(EditorWindow).Assembly.GetTypes().FirstOrDefault(ty => ty.Name == "ColorPicker");
659 var m = t?.GetMethod("Show", new[] { typeof(Action<Color>), typeof(Color), typeof(bool), typeof(bool) });
660 if (m == null)
661 {
662 Debug.LogWarning("Could not invoke Color Picker for ShaderGraph.");
663 return;
664 }
665
666 var editorView = GetFirstAncestorOfType<GraphEditorView>();
667 var defaultColor = Color.gray;
668 if (selection.FirstOrDefault(sel => sel is MaterialNodeView) is MaterialNodeView selNode1)
669 {
670 defaultColor = selNode1.GetColor();
671 defaultColor.a = 1.0f;
672 }
673
674 void ApplyColor(Color pickedColor)
675 {
676 foreach (var selectable in selection)
677 {
678 if (selectable is MaterialNodeView nodeView)
679 {
680 nodeView.node.SetColor(editorView.colorManager.activeProviderName, pickedColor);
681 editorView.colorManager.UpdateNodeView(nodeView);
682 }
683 }
684 }
685
686 graph.owner.RegisterCompleteObjectUndo("Change Node Color");
687 m.Invoke(null, new object[] { (Action<Color>)ApplyColor, defaultColor, true, false });
688 }
689
690 protected internal override bool canDeleteSelection
691 {
692 get
693 {
694 return selection.Any(x =>
695 {
696 if (x is ContextView) return false; //< context view must not be deleted. ( eg, Vertex, Fragment )
697 return !(x is IShaderNodeView nodeView) || nodeView.node.canDeleteNode;
698 });
699 }
700 }
701 public void GroupSelection()
702 {
703 var title = "New Group";
704 var groupData = new GroupData(title, new Vector2(10f, 10f));
705
706 graph.owner.RegisterCompleteObjectUndo("Create Group Node");
707 graph.CreateGroup(groupData);
708
709 foreach (var element in selection.OfType<GraphElement>())
710 {
711 if (element.userData is IGroupItem groupItem)
712 {
713 graph.SetGroup(groupItem, groupData);
714 }
715 }
716 }
717
718 public void AddStickyNote(Vector2 position)
719 {
720 position = contentViewContainer.WorldToLocal(position);
721 string title = "New Note";
722 string content = "Write something here";
723 var stickyNoteData = new StickyNoteData(title, content, new Rect(position.x, position.y, 200, 160));
724 graph.owner.RegisterCompleteObjectUndo("Create Sticky Note");
725 graph.AddStickyNote(stickyNoteData);
726 }
727
728 public void RemoveFromGroupNode()
729 {
730 graph.owner.RegisterCompleteObjectUndo("Ungroup Node(s)");
731 foreach (var element in selection.OfType<GraphElement>())
732 {
733 if (element.userData is IGroupItem)
734 {
735 Group group = element.GetContainingScope() as Group;
736 if (group != null)
737 {
738 group.RemoveElement(element);
739 }
740 }
741 }
742 }
743
744 public void SetNodeExpandedForSelectedNodes(bool state, bool recordUndo = true)
745 {
746 if (recordUndo)
747 {
748 graph.owner.RegisterCompleteObjectUndo(state ? "Expand Nodes" : "Collapse Nodes");
749 }
750
751 foreach (MaterialNodeView selectedNode in selection.Where(x => x is MaterialNodeView).Select(x => x as MaterialNodeView))
752 {
753 if (selectedNode.CanToggleNodeExpanded() && selectedNode.expanded != state)
754 {
755 selectedNode.expanded = state;
756 selectedNode.node.Dirty(ModificationScope.Topological);
757 }
758 }
759 }
760
761 public void SetPreviewExpandedForSelectedNodes(bool state)
762 {
763 graph.owner.RegisterCompleteObjectUndo(state ? "Expand Nodes" : "Collapse Nodes");
764
765 foreach (MaterialNodeView selectedNode in selection.Where(x => x is MaterialNodeView).Select(x => x as MaterialNodeView))
766 {
767 selectedNode.node.previewExpanded = state;
768 }
769 }
770
771 public void SetNodePrecisionOnSelection(Precision inPrecision)
772 {
773 var editorView = GetFirstAncestorOfType<GraphEditorView>();
774 IEnumerable<MaterialNodeView> nodes = selection.Where(x => x is MaterialNodeView node && node.node.canSetPrecision).Select(x => x as MaterialNodeView);
775
776 graph.owner.RegisterCompleteObjectUndo("Set Precisions");
777 editorView.colorManager.SetNodesDirty(nodes);
778
779 foreach (MaterialNodeView selectedNode in nodes)
780 {
781 selectedNode.node.precision = inPrecision;
782 }
783
784 // Reflect the data down
785 graph.ValidateGraph();
786 editorView.colorManager.UpdateNodeViews(nodes);
787 m_InspectorUpdateDelegate?.Invoke();
788
789 // Update the views
790 foreach (MaterialNodeView selectedNode in nodes)
791 selectedNode.node.Dirty(ModificationScope.Topological);
792 }
793
794 void CollapsePreviews(DropdownMenuAction action)
795 {
796 graph.owner.RegisterCompleteObjectUndo("Collapse Previews");
797
798 foreach (AbstractMaterialNode node in graph.GetNodes<AbstractMaterialNode>())
799 {
800 node.previewExpanded = false;
801 }
802 }
803
804 void ExpandPreviews(DropdownMenuAction action)
805 {
806 graph.owner.RegisterCompleteObjectUndo("Expand Previews");
807
808 foreach (AbstractMaterialNode node in graph.GetNodes<AbstractMaterialNode>())
809 {
810 node.previewExpanded = true;
811 }
812 }
813
814 void SeeDocumentation(DropdownMenuAction action)
815 {
816 var node = selection.OfType<IShaderNodeView>().First().node;
817 if (node.documentationURL != null)
818 System.Diagnostics.Process.Start(node.documentationURL);
819 }
820
821 void OpenSubGraph(DropdownMenuAction action)
822 {
823 SubGraphNode subgraphNode = selection.OfType<IShaderNodeView>().First().node as SubGraphNode;
824
825 var path = AssetDatabase.GetAssetPath(subgraphNode.asset);
826 ShaderGraphImporterEditor.ShowGraphEditWindow(path);
827 }
828
829 DropdownMenuAction.Status SeeDocumentationStatus(DropdownMenuAction action)
830 {
831 if (selection.OfType<IShaderNodeView>().First().node.documentationURL == null)
832 return DropdownMenuAction.Status.Disabled;
833 return DropdownMenuAction.Status.Normal;
834 }
835
836 DropdownMenuAction.Status ConvertToPropertyStatus(DropdownMenuAction action)
837 {
838 if (selection.OfType<IShaderNodeView>().Any(v => v.node != null))
839 {
840 if (selection.OfType<IShaderNodeView>().Any(v => v.node is IPropertyFromNode))
841 return DropdownMenuAction.Status.Normal;
842 return DropdownMenuAction.Status.Disabled;
843 }
844 return DropdownMenuAction.Status.Hidden;
845 }
846
847 void ConvertToProperty(DropdownMenuAction action)
848 {
849 var convertToPropertyAction = new ConvertToPropertyAction();
850
851 var selectedNodeViews = selection.OfType<IShaderNodeView>().Select(x => x.node).ToList();
852 foreach (var node in selectedNodeViews)
853 {
854 if (!(node is IPropertyFromNode))
855 continue;
856
857 var converter = node as IPropertyFromNode;
858 convertToPropertyAction.inlinePropertiesToConvert.Add(converter);
859 }
860
861 graph.owner.graphDataStore.Dispatch(convertToPropertyAction);
862 }
863
864 DropdownMenuAction.Status ConvertToInlineNodeStatus(DropdownMenuAction action)
865 {
866 if (selection.OfType<IShaderNodeView>().Any(v => v.node != null))
867 {
868 if (selection.OfType<IShaderNodeView>().Any(v => v.node is PropertyNode))
869 return DropdownMenuAction.Status.Normal;
870 return DropdownMenuAction.Status.Disabled;
871 }
872 return DropdownMenuAction.Status.Hidden;
873 }
874
875 void ConvertToInlineNode(DropdownMenuAction action)
876 {
877 var selectedNodeViews = selection.OfType<IShaderNodeView>()
878 .Select(x => x.node)
879 .OfType<PropertyNode>();
880
881 var convertToInlineAction = new ConvertToInlineAction();
882 convertToInlineAction.propertyNodesToConvert = selectedNodeViews;
883 graph.owner.graphDataStore.Dispatch(convertToInlineAction);
884 }
885
886 // Made internal for purposes of UI testing
887 internal void DuplicateSelection()
888 {
889 graph.owner.RegisterCompleteObjectUndo("Duplicate Blackboard Selection");
890
891 List<ShaderInput> selectedProperties = new List<ShaderInput>();
892 List<CategoryData> selectedCategories = new List<CategoryData>();
893
894 for (int index = 0; index < selection.Count; ++index)
895 {
896 var selectable = selection[index];
897 if (selectable is SGBlackboardCategory blackboardCategory)
898 {
899 selectedCategories.Add(blackboardCategory.controller.Model);
900 var childBlackboardFields = blackboardCategory.Query<SGBlackboardField>().ToList();
901 // Remove the children that live in this category (if any) from the selection, as they will get copied twice otherwise
902 selection.RemoveAll(childItem => childBlackboardFields.Contains(childItem));
903 }
904 }
905
906 foreach (var selectable in selection)
907 {
908 if (selectable is SGBlackboardField blackboardField)
909 {
910 selectedProperties.Add(blackboardField.controller.Model);
911 }
912 }
913
914 // Sort so that the ShaderInputs are in the correct order
915 selectedProperties.Sort((x, y) => graph.GetGraphInputIndex(x) > graph.GetGraphInputIndex(y) ? 1 : -1);
916
917 CopyPasteGraph copiedItems = new CopyPasteGraph(null, null, null, selectedProperties, selectedCategories, null, null, null, null, copyPasteGraphSource: CopyPasteGraphSource.Duplicate);
918 GraphViewExtensions.InsertCopyPasteGraph(this, copiedItems);
919 }
920
921 DropdownMenuAction.Status ConvertToSubgraphStatus(DropdownMenuAction action)
922 {
923 if (onConvertToSubgraphClick == null) return DropdownMenuAction.Status.Hidden;
924 return selection.OfType<IShaderNodeView>().Any(v => v.node != null && v.node.allowedInSubGraph && !(v.node is SubGraphOutputNode)) ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Hidden;
925 }
926
927 void ConvertToSubgraph(DropdownMenuAction action)
928 {
929 onConvertToSubgraphClick();
930 }
931
932 string SerializeGraphElementsImplementation(IEnumerable<GraphElement> elements)
933 {
934 var groups = elements.OfType<ShaderGroup>().Select(x => x.userData);
935 var nodes = elements.OfType<IShaderNodeView>().Select(x => x.node).Where(x => x.canCopyNode);
936 var edges = elements.OfType<Edge>().Select(x => (Graphing.Edge)x.userData);
937 var notes = elements.OfType<StickyNote>().Select(x => x.userData);
938
939 var categories = new List<CategoryData>();
940 foreach (var selectable in selection)
941 {
942 if (selectable is SGBlackboardCategory blackboardCategory)
943 {
944 categories.Add(blackboardCategory.userData as CategoryData);
945 }
946 }
947
948 var inputs = selection.OfType<SGBlackboardField>().Select(x => x.userData as ShaderInput).ToList();
949
950 // Collect the property nodes and get the corresponding properties
951 var metaProperties = new HashSet<AbstractShaderProperty>(nodes.OfType<PropertyNode>().Select(x => x.property).Concat(inputs.OfType<AbstractShaderProperty>()));
952
953 // Collect the keyword nodes and get the corresponding keywords
954 var metaKeywords = new HashSet<ShaderKeyword>(nodes.OfType<KeywordNode>().Select(x => x.keyword).Concat(inputs.OfType<ShaderKeyword>()));
955
956 // Collect the dropdown nodes and get the corresponding dropdowns
957 var metaDropdowns = new HashSet<ShaderDropdown>(nodes.OfType<DropdownNode>().Select(x => x.dropdown).Concat(inputs.OfType<ShaderDropdown>()));
958
959 // Sort so that the ShaderInputs are in the correct order
960 inputs.Sort((x, y) => graph.GetGraphInputIndex(x) > graph.GetGraphInputIndex(y) ? 1 : -1);
961
962 var copyPasteGraph = new CopyPasteGraph(groups, nodes, edges, inputs, categories, metaProperties, metaKeywords, metaDropdowns, notes);
963 return MultiJson.Serialize(copyPasteGraph);
964 }
965
966 bool CanPasteSerializedDataImplementation(string serializedData)
967 {
968 return CopyPasteGraph.FromJson(serializedData, graph) != null;
969 }
970
971 void UnserializeAndPasteImplementation(string operationName, string serializedData)
972 {
973 graph.owner.RegisterCompleteObjectUndo(operationName);
974
975 var pastedGraph = CopyPasteGraph.FromJson(serializedData, graph);
976 this.InsertCopyPasteGraph(pastedGraph);
977 }
978
979 void DeleteSelectionImplementation(string operationName, GraphView.AskUser askUser)
980 {
981 // Selection state of Graph elements and the Focus state of UIElements are not mutually exclusive.
982 // For Hotkeys, askUser should be AskUser mode, which should early out so that the focused Element can win.
983 if (this.focusController.focusedElement != null
984 && focusController.focusedElement is UIElements.ObjectField
985 && askUser == GraphView.AskUser.AskUser)
986 {
987 return;
988 }
989
990 bool containsProperty = false;
991
992 // Keywords need to be tested against variant limit based on multiple factors
993 bool keywordsDirty = false;
994 bool dropdownsDirty = false;
995
996 // Track dependent keyword nodes to remove them
997 List<KeywordNode> keywordNodes = new List<KeywordNode>();
998 List<DropdownNode> dropdownNodes = new List<DropdownNode>();
999
1000 foreach (var selectable in selection)
1001 {
1002 if (selectable is SGBlackboardField propertyView && propertyView.userData != null)
1003 {
1004 switch (propertyView.userData)
1005 {
1006 case AbstractShaderProperty property:
1007 containsProperty = true;
1008 break;
1009 case ShaderKeyword keyword:
1010 keywordNodes.AddRange(graph.GetNodes<KeywordNode>().Where(x => x.keyword == keyword));
1011 break;
1012 case ShaderDropdown dropdown:
1013 dropdownNodes.AddRange(graph.GetNodes<DropdownNode>().Where(x => x.dropdown == dropdown));
1014 break;
1015 default:
1016 throw new ArgumentOutOfRangeException();
1017 }
1018 }
1019 }
1020
1021 if (containsProperty)
1022 {
1023 if (graph.isSubGraph)
1024 {
1025 if (!EditorUtility.DisplayDialog("Sub Graph Will Change", "If you remove a property and save the sub graph, you might change other graphs that are using this sub graph.\n\nDo you want to continue?", "Yes", "No"))
1026 return;
1027 }
1028 }
1029
1030 // Filter nodes that cannot be deleted
1031 var nodesToDelete = selection.OfType<IShaderNodeView>().Where(v => !(v.node is SubGraphOutputNode) && v.node.canDeleteNode).Select(x => x.node);
1032
1033 // Add keyword nodes dependent on deleted keywords
1034 nodesToDelete = nodesToDelete.Union(keywordNodes);
1035 nodesToDelete = nodesToDelete.Union(dropdownNodes);
1036
1037 // If deleting a Sub Graph node whose asset contains Keywords test variant limit
1038 foreach (SubGraphNode subGraphNode in nodesToDelete.OfType<SubGraphNode>())
1039 {
1040 if (subGraphNode.asset == null)
1041 {
1042 continue;
1043 }
1044 if (subGraphNode.asset.keywords.Any())
1045 {
1046 keywordsDirty = true;
1047 }
1048 if (subGraphNode.asset.dropdowns.Any())
1049 {
1050 dropdownsDirty = true;
1051 }
1052 }
1053
1054 graph.owner.RegisterCompleteObjectUndo(operationName);
1055 graph.RemoveElements(nodesToDelete.ToArray(),
1056 selection.OfType<Edge>().Select(x => x.userData).OfType<IEdge>().ToArray(),
1057 selection.OfType<ShaderGroup>().Select(x => x.userData).ToArray(),
1058 selection.OfType<StickyNote>().Select(x => x.userData).ToArray());
1059
1060
1061 var copiedSelectionList = new List<ISelectable>(selection);
1062 var deleteShaderInputAction = new DeleteShaderInputAction();
1063 var deleteCategoriesAction = new DeleteCategoryAction();
1064
1065 for (int index = 0; index < copiedSelectionList.Count; ++index)
1066 {
1067 var selectable = copiedSelectionList[index];
1068 if (selectable is SGBlackboardField field && field.userData != null)
1069 {
1070 var input = (ShaderInput)field.userData;
1071 deleteShaderInputAction.shaderInputsToDelete.Add(input);
1072
1073 // If deleting a Keyword test variant limit
1074 if (input is ShaderKeyword keyword)
1075 {
1076 keywordsDirty = true;
1077 }
1078 if (input is ShaderDropdown dropdown)
1079 {
1080 dropdownsDirty = true;
1081 }
1082 }
1083 // Don't allow the default category to be deleted
1084 else if (selectable is SGBlackboardCategory category && category.controller.Model.IsNamedCategory())
1085 {
1086 deleteCategoriesAction.categoriesToRemoveGuids.Add(category.viewModel.associatedCategoryGuid);
1087 }
1088 }
1089
1090 if (deleteShaderInputAction.shaderInputsToDelete.Count != 0)
1091 graph.owner.graphDataStore.Dispatch(deleteShaderInputAction);
1092 if (deleteCategoriesAction.categoriesToRemoveGuids.Count != 0)
1093 graph.owner.graphDataStore.Dispatch(deleteCategoriesAction);
1094
1095 // Test Keywords against variant limit
1096 if (keywordsDirty)
1097 {
1098 graph.OnKeywordChangedNoValidate();
1099 }
1100 if (dropdownsDirty)
1101 {
1102 graph.OnDropdownChangedNoValidate();
1103 }
1104
1105 selection.Clear();
1106 m_InspectorUpdateDelegate?.Invoke();
1107 }
1108
1109 // Updates selected graph elements after undo/redo
1110 internal void RestorePersistentSelectionAfterUndoRedo()
1111 {
1112 wasUndoRedoPerformed = true;
1113 UndoRedoInfo info = new UndoRedoInfo();
1114 m_UndoRedoPerformedMethodInfo?.Invoke(this, new object[] {info});
1115 }
1116
1117 #region Drag and drop
1118
1119 bool ValidateObjectForDrop(Object obj)
1120 {
1121 return EditorUtility.IsPersistent(obj) && (
1122 obj is Texture2D ||
1123 obj is Cubemap ||
1124 obj is SubGraphAsset asset && !asset.descendents.Contains(graph.assetGuid) && asset.assetGuid != graph.assetGuid ||
1125 obj is Texture2DArray ||
1126 obj is Texture3D);
1127 }
1128
1129 void OnDragUpdatedEvent(DragUpdatedEvent e)
1130 {
1131 var selection = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>;
1132 bool dragging = false;
1133 if (selection != null)
1134 {
1135 var anyCategoriesInSelection = selection.OfType<SGBlackboardCategory>();
1136 if (!anyCategoriesInSelection.Any())
1137 {
1138 // Blackboard items
1139 bool validFields = false;
1140 foreach (SGBlackboardField propertyView in selection.OfType<SGBlackboardField>())
1141 {
1142 if (!(propertyView.userData is MultiJsonInternal.UnknownShaderPropertyType))
1143 validFields = true;
1144 }
1145
1146 dragging = validFields;
1147 }
1148 else
1149 DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
1150 }
1151 else
1152 {
1153 // Handle unity objects
1154 var objects = DragAndDrop.objectReferences;
1155 foreach (Object obj in objects)
1156 {
1157 if (ValidateObjectForDrop(obj))
1158 {
1159 dragging = true;
1160 break;
1161 }
1162 }
1163 }
1164
1165 if (dragging)
1166 {
1167 DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
1168 }
1169 }
1170
1171 // Contrary to the name this actually handles when the drop operation is performed
1172 void OnDragPerformEvent(DragPerformEvent e)
1173 {
1174 Vector2 localPos = (e.currentTarget as VisualElement).ChangeCoordinatesTo(contentViewContainer, e.localMousePosition);
1175
1176 var selection = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>;
1177 if (selection != null)
1178 {
1179 // Blackboard
1180 if (selection.OfType<SGBlackboardField>().Any())
1181 {
1182 IEnumerable<SGBlackboardField> fields = selection.OfType<SGBlackboardField>();
1183 foreach (SGBlackboardField field in fields)
1184 {
1185 CreateNode(field, localPos);
1186 }
1187
1188 // Call this delegate so blackboard can respond to blackboard field being dropped
1189 blackboardFieldDropDelegate?.Invoke();
1190 }
1191 }
1192 else
1193 {
1194 // Handle unity objects
1195 var objects = DragAndDrop.objectReferences;
1196 foreach (Object obj in objects)
1197 {
1198 if (ValidateObjectForDrop(obj))
1199 {
1200 CreateNode(obj, localPos);
1201 }
1202 }
1203 }
1204 }
1205
1206 void OnMouseMoveEvent(MouseMoveEvent evt)
1207 {
1208 this.cachedMousePosition = evt.mousePosition;
1209 }
1210
1211 void CreateNode(object obj, Vector2 nodePosition)
1212 {
1213 var texture2D = obj as Texture2D;
1214 if (texture2D != null)
1215 {
1216 graph.owner.RegisterCompleteObjectUndo("Drag Texture");
1217
1218 bool isNormalMap = false;
1219 if (EditorUtility.IsPersistent(texture2D) && !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(texture2D)))
1220 {
1221 var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(texture2D)) as TextureImporter;
1222 if (importer != null)
1223 isNormalMap = importer.textureType == TextureImporterType.NormalMap;
1224 }
1225
1226 var node = new SampleTexture2DNode();
1227 var drawState = node.drawState;
1228 drawState.position = new Rect(nodePosition, drawState.position.size);
1229 node.drawState = drawState;
1230 graph.AddNode(node);
1231
1232 if (isNormalMap)
1233 node.textureType = TextureType.Normal;
1234
1235 var inputslot = node.FindInputSlot<Texture2DInputMaterialSlot>(SampleTexture2DNode.TextureInputId);
1236 if (inputslot != null)
1237 inputslot.texture = texture2D;
1238 }
1239
1240 var textureArray = obj as Texture2DArray;
1241 if (textureArray != null)
1242 {
1243 graph.owner.RegisterCompleteObjectUndo("Drag Texture Array");
1244
1245 var node = new SampleTexture2DArrayNode();
1246 var drawState = node.drawState;
1247 drawState.position = new Rect(nodePosition, drawState.position.size);
1248 node.drawState = drawState;
1249 graph.AddNode(node);
1250
1251 var inputslot = node.FindSlot<Texture2DArrayInputMaterialSlot>(SampleTexture2DArrayNode.TextureInputId);
1252 if (inputslot != null)
1253 inputslot.textureArray = textureArray;
1254 }
1255
1256 var texture3D = obj as Texture3D;
1257 if (texture3D != null)
1258 {
1259 graph.owner.RegisterCompleteObjectUndo("Drag Texture 3D");
1260
1261 var node = new SampleTexture3DNode();
1262 var drawState = node.drawState;
1263 drawState.position = new Rect(nodePosition, drawState.position.size);
1264 node.drawState = drawState;
1265 graph.AddNode(node);
1266
1267 var inputslot = node.FindSlot<Texture3DInputMaterialSlot>(SampleTexture3DNode.TextureInputId);
1268 if (inputslot != null)
1269 inputslot.texture = texture3D;
1270 }
1271
1272 var cubemap = obj as Cubemap;
1273 if (cubemap != null)
1274 {
1275 graph.owner.RegisterCompleteObjectUndo("Drag Cubemap");
1276
1277 var node = new SampleCubemapNode();
1278 var drawState = node.drawState;
1279 drawState.position = new Rect(nodePosition, drawState.position.size);
1280 node.drawState = drawState;
1281 graph.AddNode(node);
1282
1283 var inputslot = node.FindInputSlot<CubemapInputMaterialSlot>(SampleCubemapNode.CubemapInputId);
1284 if (inputslot != null)
1285 inputslot.cubemap = cubemap;
1286 }
1287
1288 var subGraphAsset = obj as SubGraphAsset;
1289 if (subGraphAsset != null)
1290 {
1291 graph.owner.RegisterCompleteObjectUndo("Drag Sub-Graph");
1292 var node = new SubGraphNode();
1293
1294 var drawState = node.drawState;
1295 drawState.position = new Rect(nodePosition, drawState.position.size);
1296 node.drawState = drawState;
1297 node.asset = subGraphAsset;
1298 graph.AddNode(node);
1299 }
1300
1301 var blackboardPropertyView = obj as SGBlackboardField;
1302 if (blackboardPropertyView?.userData is ShaderInput inputBeingDraggedIn)
1303 {
1304 var dragGraphInputAction = new DragGraphInputAction { nodePosition = nodePosition, graphInputBeingDraggedIn = inputBeingDraggedIn };
1305 graph.owner.graphDataStore.Dispatch(dragGraphInputAction);
1306 }
1307 }
1308
1309 #endregion
1310
1311 void ElementsInsertedToStackNode(StackNode stackNode, int insertIndex, IEnumerable<GraphElement> elements)
1312 {
1313 var contextView = stackNode as ContextView;
1314 contextView.InsertElements(insertIndex, elements);
1315 }
1316 }
1317
1318 static class GraphViewExtensions
1319 {
1320 // Sorts based on their position on the blackboard
1321 internal class PropertyOrder : IComparer<ShaderInput>
1322 {
1323 GraphData graphData;
1324
1325 internal PropertyOrder(GraphData data)
1326 {
1327 graphData = data;
1328 }
1329
1330 public int Compare(ShaderInput x, ShaderInput y)
1331 {
1332 if (graphData.GetGraphInputIndex(x) > graphData.GetGraphInputIndex(y)) return 1;
1333 else return -1;
1334 }
1335 }
1336
1337 internal static void InsertCopyPasteGraph(this MaterialGraphView graphView, CopyPasteGraph copyGraph)
1338 {
1339 if (copyGraph == null)
1340 return;
1341
1342 // Keywords need to be tested against variant limit based on multiple factors
1343 bool keywordsDirty = false;
1344
1345 bool dropdownsDirty = false;
1346
1347 var blackboardController = graphView.GetFirstAncestorOfType<GraphEditorView>().blackboardController;
1348
1349 // Get the position to insert the new shader inputs per category
1350 int insertionIndex = blackboardController.GetInsertionIndexForPaste();
1351
1352 // Any child of the categories need to be removed from selection as well (there's a Graphview issue where these don't get properly added to selection before the duplication sometimes, have to do it manually)
1353 foreach (var selectable in graphView.selection)
1354 {
1355 if (selectable is SGBlackboardCategory blackboardCategory)
1356 {
1357 foreach (var blackboardChild in blackboardCategory.Children())
1358 {
1359 if (blackboardChild is SGBlackboardRow blackboardRow)
1360 {
1361 var blackboardField = blackboardRow.Q<SGBlackboardField>();
1362 if (blackboardField != null)
1363 {
1364 blackboardField.selected = false;
1365 blackboardField.OnUnselected();
1366 }
1367 }
1368 }
1369 }
1370 }
1371
1372 var cachedSelection = graphView.selection.ToList();
1373
1374 // Before copy-pasting, clear all current selections so the duplicated items may be selected instead
1375 graphView.ClearSelectionNoUndoRecord();
1376
1377 // Make new inputs from the copied graph
1378 foreach (ShaderInput input in copyGraph.inputs)
1379 {
1380 // Disregard any inputs that belong to a category in the CopyPasteGraph already,
1381 // GraphData handles copying those child inputs over when the category is copied
1382 if (copyGraph.IsInputCategorized(input))
1383 continue;
1384
1385 string associatedCategoryGuid = String.Empty;
1386
1387 foreach (var category in graphView.graph.categories)
1388 {
1389 if (copyGraph.IsInputDuplicatedFromCategory(input, category, graphView.graph))
1390 {
1391 associatedCategoryGuid = category.categoryGuid;
1392 }
1393 }
1394
1395 // In the specific case of just an input being selected to copy but some other category than the one containing it was selected, we want to copy it to the default category
1396 if (associatedCategoryGuid != String.Empty)
1397 {
1398 foreach (var selection in cachedSelection)
1399 {
1400 if (selection is SGBlackboardCategory blackboardCategory && blackboardCategory.viewModel.associatedCategoryGuid != associatedCategoryGuid)
1401 {
1402 associatedCategoryGuid = String.Empty;
1403 // Also ensures it is added to the end of the default category
1404 insertionIndex = -1;
1405 }
1406 }
1407 }
1408
1409 // This ensures that if an item is duplicated, it is copied to the same category
1410 if (copyGraph.copyPasteGraphSource == CopyPasteGraphSource.Duplicate)
1411 {
1412 associatedCategoryGuid = graphView.graph.FindCategoryForInput(input);
1413 }
1414
1415 var copyShaderInputAction = new CopyShaderInputAction { shaderInputToCopy = input, containingCategoryGuid = associatedCategoryGuid };
1416 copyShaderInputAction.insertIndex = insertionIndex;
1417
1418 if (graphView.graph.IsInputAllowedInGraph(input))
1419 {
1420 switch (input)
1421 {
1422 case AbstractShaderProperty property:
1423 copyShaderInputAction.dependentNodeList = copyGraph.GetNodes<PropertyNode>().Where(x => x.property == input);
1424 break;
1425
1426 case ShaderKeyword shaderKeyword:
1427 copyShaderInputAction.dependentNodeList = copyGraph.GetNodes<KeywordNode>().Where(x => x.keyword == input);
1428 // Pasting a new Keyword so need to test against variant limit
1429 keywordsDirty = true;
1430 break;
1431
1432 case ShaderDropdown shaderDropdown:
1433 copyShaderInputAction.dependentNodeList = copyGraph.GetNodes<DropdownNode>().Where(x => x.dropdown == input);
1434 dropdownsDirty = true;
1435 break;
1436
1437 default:
1438 AssertHelpers.Fail("Tried to paste shader input of unknown type into graph.");
1439 break;
1440 }
1441
1442 graphView.graph.owner.graphDataStore.Dispatch(copyShaderInputAction);
1443
1444 // Increment insertion index for next input
1445 insertionIndex++;
1446 }
1447 }
1448
1449 // Make new categories from the copied graph
1450 foreach (var category in copyGraph.categories)
1451 {
1452 foreach (var input in category.Children.ToList())
1453 {
1454 // Remove this input from being copied if its not allowed to be copied into the target graph (eg. its a dropdown and the target graph isn't a sub-graph)
1455 if (graphView.graph.IsInputAllowedInGraph(input) == false)
1456 category.RemoveItemFromCategory(input);
1457 }
1458 var copyCategoryAction = new CopyCategoryAction() { categoryToCopyReference = category };
1459 graphView.graph.owner.graphDataStore.Dispatch(copyCategoryAction);
1460 }
1461
1462 // Pasting a Sub Graph node that contains Keywords so need to test against variant limit
1463 foreach (SubGraphNode subGraphNode in copyGraph.GetNodes<SubGraphNode>())
1464 {
1465 if (subGraphNode.asset.keywords.Any())
1466 {
1467 keywordsDirty = true;
1468 }
1469
1470 if (subGraphNode.asset.dropdowns.Any())
1471 {
1472 dropdownsDirty = true;
1473 }
1474 }
1475
1476 // Test Keywords against variant limit
1477 if (keywordsDirty)
1478 {
1479 graphView.graph.OnKeywordChangedNoValidate();
1480 }
1481
1482 if (dropdownsDirty)
1483 {
1484 graphView.graph.OnDropdownChangedNoValidate();
1485 }
1486
1487 using (ListPool<AbstractMaterialNode>.Get(out var remappedNodes))
1488 {
1489 using (ListPool<Graphing.Edge>.Get(out var remappedEdges))
1490 {
1491 var nodeList = copyGraph.GetNodes<AbstractMaterialNode>();
1492
1493 ClampNodesWithinView(graphView,
1494 new List<IRectInterface>()
1495 .Union(nodeList)
1496 .Union(copyGraph.stickyNotes)
1497 .Union(copyGraph.groups)
1498 );
1499
1500 graphView.graph.PasteGraph(copyGraph, remappedNodes, remappedEdges);
1501
1502 // Add new elements to selection
1503 graphView.graphElements.ForEach(element =>
1504 {
1505 if (element is Edge edge && remappedEdges.Contains(edge.userData as IEdge))
1506 graphView.AddToSelection(edge);
1507
1508 if (element is IShaderNodeView nodeView && remappedNodes.Contains(nodeView.node))
1509 graphView.AddToSelection((Node)nodeView);
1510 });
1511 }
1512 }
1513 }
1514
1515 private static void ClampNodesWithinView(MaterialGraphView graphView, IEnumerable<IRectInterface> rectList)
1516 {
1517 // Compute the centroid of the copied elements at their original positions
1518 var positions = rectList.Select(n => n.rect.position);
1519 var centroid = UIUtilities.CalculateCentroid(positions);
1520
1521 /* Ensure nodes get pasted at cursor */
1522 var graphMousePosition = graphView.contentViewContainer.WorldToLocal(graphView.cachedMousePosition);
1523 var copiedNodesOrigin = graphMousePosition;
1524 float xMin = float.MaxValue, xMax = float.MinValue, yMin = float.MaxValue, yMax = float.MinValue;
1525
1526 // Calculate bounding rectangle min and max coordinates for these elements, to use in clamping later
1527 foreach (var element in rectList)
1528 {
1529 var position = element.rect.position;
1530 xMin = Mathf.Min(xMin, position.x);
1531 yMin = Mathf.Min(yMin, position.y);
1532 xMax = Mathf.Max(xMax, position.x);
1533 yMax = Mathf.Max(yMax, position.y);
1534 }
1535
1536 // Get center of the current view
1537 var center = graphView.contentViewContainer.WorldToLocal(graphView.layout.center);
1538 // Get offset from center of view to mouse position
1539 var mouseOffset = center - graphMousePosition;
1540
1541 var zoomAdjustedViewScale = 1.0f / graphView.scale;
1542 var graphViewScaledHalfWidth = (graphView.layout.width * zoomAdjustedViewScale) / 2.0f;
1543 var graphViewScaledHalfHeight = (graphView.layout.height * zoomAdjustedViewScale) / 2.0f;
1544 const float widthThreshold = 40.0f;
1545 const float heightThreshold = 20.0f;
1546
1547 if ((Mathf.Abs(mouseOffset.x) + widthThreshold > graphViewScaledHalfWidth ||
1548 (Mathf.Abs(mouseOffset.y) + heightThreshold > graphViewScaledHalfHeight)))
1549 {
1550 // Out of bounds - Adjust taking into account the size of the bounding box around elements and the current graph zoom level
1551 var adjustedPositionX = (xMax - xMin) + widthThreshold * zoomAdjustedViewScale;
1552 var adjustedPositionY = (yMax - yMin) + heightThreshold * zoomAdjustedViewScale;
1553 adjustedPositionY *= -1.0f * Mathf.Sign(copiedNodesOrigin.y);
1554 adjustedPositionX *= -1.0f * Mathf.Sign(copiedNodesOrigin.x);
1555 copiedNodesOrigin.x += adjustedPositionX;
1556 copiedNodesOrigin.y += adjustedPositionY;
1557 }
1558
1559 foreach (var element in rectList)
1560 {
1561 var rect = element.rect;
1562
1563 // Get the relative offset from the calculated centroid
1564 var relativeOffsetFromCentroid = rect.position - centroid;
1565 // Reapply that offset to ensure element positions are consistent when multiple elements are copied
1566 rect.x = copiedNodesOrigin.x + relativeOffsetFromCentroid.x;
1567 rect.y = copiedNodesOrigin.y + relativeOffsetFromCentroid.y;
1568 element.rect = rect;
1569 }
1570 }
1571 }
1572}