A game about forced loneliness, made by TACStudios
at master 70 kB view raw
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}