A game about forced loneliness, made by TACStudios
at master 56 kB view raw
1using System; 2using System.Collections.Generic; 3using System.IO; 4using System.Linq; 5using System.Text; 6using UnityEngine; 7using UnityEditor.Graphing; 8using UnityEditor.Graphing.Util; 9using Debug = UnityEngine.Debug; 10using Object = UnityEngine.Object; 11using UnityEngine.Rendering; 12 13using UnityEditor.UIElements; 14using Edge = UnityEditor.Experimental.GraphView.Edge; 15using UnityEditor.Experimental.GraphView; 16using UnityEditor.ShaderGraph.Internal; 17using UnityEditor.ShaderGraph.Serialization; 18using UnityEngine.UIElements; 19using UnityEditor.VersionControl; 20 21using Unity.Profiling; 22using UnityEngine.Assertions; 23 24namespace UnityEditor.ShaderGraph.Drawing 25{ 26 class MaterialGraphEditWindow : EditorWindow 27 { 28 // For conversion to Sub Graph: keys for remembering the user's desired path 29 const string k_PrevSubGraphPathKey = "SHADER_GRAPH_CONVERT_TO_SUB_GRAPH_PATH"; 30 const string k_PrevSubGraphPathDefaultValue = "?"; // Special character that NTFS does not allow, so that no directory could match it. 31 32 [SerializeField] 33 string m_Selected; 34 35 [SerializeField] 36 GraphObject m_GraphObject; 37 38 // this stores the contents of the file on disk, as of the last time we saved or loaded it from disk 39 [SerializeField] 40 string m_LastSerializedFileContents; 41 42 [NonSerialized] 43 HashSet<string> m_ChangedFileDependencyGUIDs = new HashSet<string>(); 44 45 ColorSpace m_ColorSpace; 46 RenderPipelineAsset m_RenderPipelineAsset; 47 48 [NonSerialized] 49 bool m_FrameAllAfterLayout; 50 [NonSerialized] 51 bool m_HasError; 52 [NonSerialized] 53 bool m_ProTheme; 54 [NonSerialized] 55 int m_customInterpWarn; 56 [NonSerialized] 57 int m_customInterpErr; 58 59 [SerializeField] 60 bool m_AssetMaybeChangedOnDisk; 61 62 [SerializeField] 63 bool m_AssetMaybeDeleted; 64 65 internal bool WereWindowResourcesDisposed { get; private set; } 66 67 MessageManager m_MessageManager; 68 MessageManager messageManager 69 { 70 get { return m_MessageManager ?? (m_MessageManager = new MessageManager()); } 71 } 72 73 GraphEditorView m_GraphEditorView; 74 internal GraphEditorView graphEditorView 75 { 76 get { return m_GraphEditorView; } 77 private set 78 { 79 if (m_GraphEditorView != null) 80 { 81 m_GraphEditorView.UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged); 82 m_GraphEditorView.RemoveFromHierarchy(); 83 m_GraphEditorView.Dispose(); 84 } 85 86 m_GraphEditorView = value; 87 88 if (m_GraphEditorView != null) 89 { 90 m_GraphEditorView.saveRequested += () => SaveAsset(); 91 m_GraphEditorView.saveAsRequested += SaveAs; 92 m_GraphEditorView.convertToSubgraphRequested += ToSubGraph; 93 m_GraphEditorView.showInProjectRequested += PingAsset; 94 m_GraphEditorView.isCheckedOut += IsGraphAssetCheckedOut; 95 m_GraphEditorView.checkOut += CheckoutAsset; 96 m_GraphEditorView.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged); 97 m_FrameAllAfterLayout = true; 98 this.rootVisualElement.Add(graphEditorView); 99 } 100 } 101 } 102 103 internal GraphObject graphObject 104 { 105 get { return m_GraphObject; } 106 set 107 { 108 if (m_GraphObject != null) 109 DestroyImmediate(m_GraphObject); 110 m_GraphObject = value; 111 } 112 } 113 114 public string selectedGuid 115 { 116 get { return m_Selected; } 117 private set 118 { 119 m_Selected = value; 120 } 121 } 122 123 public string assetName 124 { 125 get { return titleContent.text; } 126 } 127 128 [field: NonSerialized] 129 internal bool isVisible { get; private set; } 130 131 bool AssetFileExists() 132 { 133 var assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid); 134 return File.Exists(assetPath); 135 } 136 137 // returns true when the graph has been successfully saved, or the user has indicated they are ok with discarding the local graph 138 // returns false when saving has failed 139 bool DisplayDeletedFromDiskDialog(bool reopen = true) 140 { 141 // first double check if we've actually been deleted 142 bool saved = false; 143 bool okToClose = false; 144 string originalAssetPath = AssetDatabase.GUIDToAssetPath(selectedGuid); 145 146 while (true) 147 { 148 int option = EditorUtility.DisplayDialogComplex( 149 "Graph removed from project", 150 "The file has been deleted or removed from the project folder.\n\n" + 151 originalAssetPath + 152 "\n\nWould you like to save your Graph Asset?", 153 "Save As...", "Cancel", "Discard Graph and Close Window"); 154 155 if (option == 0) 156 { 157 string savedPath = SaveAsImplementation(false); 158 if (savedPath != null) 159 { 160 saved = true; 161 162 // either close or reopen the local window editor 163 graphObject = null; 164 selectedGuid = (reopen ? AssetDatabase.AssetPathToGUID(savedPath) : null); 165 166 break; 167 } 168 } 169 else if (option == 2) 170 { 171 okToClose = true; 172 graphObject = null; 173 selectedGuid = null; 174 break; 175 } 176 else if (option == 1) 177 { 178 // continue in deleted state... 179 break; 180 } 181 } 182 183 return (saved || okToClose); 184 } 185 186 void Update() 187 { 188 if (m_HasError) 189 return; 190 191 bool updateTitle = false; 192 193 if (m_AssetMaybeDeleted) 194 { 195 m_AssetMaybeDeleted = false; 196 if (AssetFileExists()) 197 { 198 // it exists... just to be sure, let's check if it changed 199 m_AssetMaybeChangedOnDisk = true; 200 } 201 else 202 { 203 // it was really deleted, ask the user what to do 204 bool handled = DisplayDeletedFromDiskDialog(true); 205 } 206 updateTitle = true; 207 } 208 209 if (PlayerSettings.colorSpace != m_ColorSpace) 210 { 211 graphEditorView = null; 212 m_ColorSpace = PlayerSettings.colorSpace; 213 } 214 215 if (GraphicsSettings.currentRenderPipeline != m_RenderPipelineAsset) 216 { 217 graphEditorView = null; 218 m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline; 219 } 220 221 if (EditorGUIUtility.isProSkin != m_ProTheme) 222 { 223 if (graphObject != null && graphObject.graph != null) 224 { 225 updateTitle = true; // trigger icon swap 226 m_ProTheme = EditorGUIUtility.isProSkin; 227 } 228 } 229 230 bool revalidate = false; 231 if (m_customInterpWarn != ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold) 232 { 233 m_customInterpWarn = ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold; 234 revalidate = true; 235 } 236 if (m_customInterpErr != ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold) 237 { 238 m_customInterpErr = ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold; 239 revalidate = true; 240 } 241 if (revalidate) 242 { 243 graphEditorView?.graphView?.graph?.ValidateGraph(); 244 } 245 246 if (m_AssetMaybeChangedOnDisk) 247 { 248 m_AssetMaybeChangedOnDisk = false; 249 250 // if we don't have any graph, then it doesn't really matter if the file on disk changed or not 251 // as we're going to reload it below anyways 252 if (graphObject?.graph != null) 253 { 254 // check if it actually did change on disk 255 if (FileOnDiskHasChanged()) 256 { 257 // don't worry people about "losing changes" unless there are changes to lose 258 bool graphChanged = GraphHasChangedSinceLastSerialization(); 259 260 if (EditorUtility.DisplayDialog( 261 "Graph has changed on disk", 262 AssetDatabase.GUIDToAssetPath(selectedGuid) + "\n\n" + 263 (graphChanged ? "Do you want to reload it and lose the changes made in the graph?" : "Do you want to reload it?"), 264 graphChanged ? "Discard Changes And Reload" : "Reload", 265 "Don't Reload")) 266 { 267 // clear graph, trigger reload 268 graphObject = null; 269 } 270 } 271 } 272 updateTitle = true; 273 } 274 275 try 276 { 277 if (graphObject == null && selectedGuid != null) 278 { 279 var guid = selectedGuid; 280 selectedGuid = null; 281 Initialize(guid); 282 } 283 284 if (graphObject == null) 285 { 286 Close(); 287 return; 288 } 289 290 var materialGraph = graphObject.graph as GraphData; 291 if (materialGraph == null) 292 return; 293 294 if (graphEditorView == null) 295 { 296 messageManager.ClearAll(); 297 materialGraph.messageManager = messageManager; 298 string assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid); 299 string graphName = Path.GetFileNameWithoutExtension(assetPath); 300 301 graphEditorView = new GraphEditorView(this, materialGraph, messageManager, graphName) 302 { 303 viewDataKey = selectedGuid, 304 }; 305 m_ColorSpace = PlayerSettings.colorSpace; 306 m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline; 307 graphObject.Validate(); 308 309 // update blackboard title for the new graphEditorView 310 updateTitle = true; 311 } 312 313 if (m_ChangedFileDependencyGUIDs.Count > 0 && graphObject != null && graphObject.graph != null) 314 { 315 bool reloadedSomething = false; 316 foreach (var guid in m_ChangedFileDependencyGUIDs) 317 { 318 if (AssetDatabase.GUIDToAssetPath(guid) != null) 319 { 320 // update preview for changed textures 321 graphEditorView?.previewManager?.ReloadChangedFiles(guid); 322 } 323 } 324 325 var subGraphNodes = graphObject.graph.GetNodes<SubGraphNode>(); 326 foreach (var subGraphNode in subGraphNodes) 327 { 328 var reloaded = subGraphNode.Reload(m_ChangedFileDependencyGUIDs); 329 reloadedSomething |= reloaded; 330 } 331 if (subGraphNodes.Count() > 0) 332 { 333 // Keywords always need to be updated to test against variant limit 334 // No Keywords may indicate removal and this may have now made the Graph valid again 335 // Need to validate Graph to clear errors in this case 336 materialGraph.OnKeywordChanged(); 337 338 UpdateDropdownEntries(); 339 materialGraph.OnDropdownChanged(); 340 } 341 foreach (var customFunctionNode in graphObject.graph.GetNodes<CustomFunctionNode>()) 342 { 343 var reloaded = customFunctionNode.Reload(m_ChangedFileDependencyGUIDs); 344 reloadedSomething |= reloaded; 345 } 346 347 // reloading files may change serialization 348 if (reloadedSomething) 349 { 350 updateTitle = true; 351 352 // may also need to re-run validation/concretization 353 graphObject.Validate(); 354 } 355 356 m_ChangedFileDependencyGUIDs.Clear(); 357 } 358 359 var wasUndoRedoPerformed = graphObject.wasUndoRedoPerformed; 360 361 if (wasUndoRedoPerformed) 362 { 363 graphEditorView.HandleGraphChanges(true); 364 graphObject.graph.ClearChanges(); 365 graphObject.HandleUndoRedo(); 366 } 367 368 if (graphObject.isDirty || wasUndoRedoPerformed) 369 { 370 updateTitle = true; 371 graphObject.isDirty = false; 372 hasUnsavedChanges = false; 373 } 374 375 // Called again to handle changes from deserialization in case an undo/redo was performed 376 graphEditorView.HandleGraphChanges(wasUndoRedoPerformed); 377 graphObject.graph.ClearChanges(); 378 379 if (wasUndoRedoPerformed) 380 { 381 graphEditorView.inspectorView.RefreshInspectables(); 382 } 383 384 if (updateTitle) 385 UpdateTitle(); 386 } 387 catch (Exception e) 388 { 389 m_HasError = true; 390 m_GraphEditorView = null; 391 graphObject = null; 392 Debug.LogException(e); 393 throw; 394 } 395 } 396 397 public void ReloadSubGraphsOnNextUpdate(List<string> changedFileGUIDs) 398 { 399 foreach (var changedFileGUID in changedFileGUIDs) 400 { 401 m_ChangedFileDependencyGUIDs.Add(changedFileGUID); 402 } 403 } 404 405 void UpdateDropdownEntries() 406 { 407 var subGraphNodes = graphObject.graph.GetNodes<SubGraphNode>(); 408 foreach (var subGraphNode in subGraphNodes) 409 { 410 var nodeView = graphEditorView.graphView.nodes.ToList().OfType<IShaderNodeView>() 411 .FirstOrDefault(p => p.node != null && p.node == subGraphNode); 412 if (nodeView != null) 413 { 414 nodeView.UpdateDropdownEntries(); 415 } 416 } 417 } 418 419 void OnEnable() 420 { 421 this.SetAntiAliasing(4); 422 } 423 424 void OnDisable() 425 { 426 messageManager.ClearAll(); 427 428 m_MessageManager = null; 429 m_RenderPipelineAsset = null; 430 431 Resources.UnloadUnusedAssets(); 432 433 WereWindowResourcesDisposed = true; 434 } 435 436 // returns true only when the file on disk doesn't match the graph we last loaded or saved to disk (i.e. someone else changed it) 437 internal bool FileOnDiskHasChanged() 438 { 439 var currentFileJson = ReadAssetFile(); 440 return !string.Equals(currentFileJson, m_LastSerializedFileContents, StringComparison.Ordinal); 441 } 442 443 // returns true only when the graph in this window would serialize different from the last time we loaded or saved it 444 internal bool GraphHasChangedSinceLastSerialization() 445 { 446 Assert.IsTrue(graphObject?.graph != null); // this should be checked by calling code 447 var currentGraphJson = MultiJson.Serialize(graphObject.graph); 448 return !string.Equals(currentGraphJson, m_LastSerializedFileContents, StringComparison.Ordinal); 449 } 450 451 // returns true only when saving the graph in this window would serialize different from the file on disk 452 internal bool GraphIsDifferentFromFileOnDisk() 453 { 454 Assert.IsTrue(graphObject?.graph != null); // this should be checked by calling code 455 var currentGraphJson = MultiJson.Serialize(graphObject.graph); 456 var currentFileJson = ReadAssetFile(); 457 return !string.Equals(currentGraphJson, currentFileJson, StringComparison.Ordinal); 458 } 459 460 // NOTE: we're using the AssetPostprocessor Asset Import and Deleted callbacks as a proxy for detecting file changes 461 // We could probably replace both m_AssetMaybeDeleted and m_AssetMaybeChangedOnDisk with a combined "need to check the real status of the file" flag 462 public void CheckForChanges() 463 { 464 if (!m_AssetMaybeDeleted && graphObject?.graph != null) 465 { 466 m_AssetMaybeChangedOnDisk = true; 467 UpdateTitle(); 468 } 469 } 470 471 public void AssetWasDeleted() 472 { 473 m_AssetMaybeDeleted = true; 474 UpdateTitle(); 475 } 476 477 public void UpdateTitle() 478 { 479 string assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid); 480 string shaderName = Path.GetFileNameWithoutExtension(assetPath); 481 482 // update blackboard title (before we add suffixes) 483 if (graphEditorView != null) 484 graphEditorView.assetName = shaderName; 485 486 // build the window title (with suffixes) 487 string title = shaderName; 488 if (graphObject?.graph == null) 489 title = title + " (nothing loaded)"; 490 else 491 { 492 if (GraphHasChangedSinceLastSerialization()) 493 { 494 hasUnsavedChanges = true; 495 // This is the message EditorWindow will show when prompting to close while dirty 496 saveChangesMessage = GetSaveChangesMessage(); 497 } 498 else 499 { 500 hasUnsavedChanges = false; 501 saveChangesMessage = ""; 502 } 503 if (!AssetFileExists()) 504 title = title + " (deleted)"; 505 } 506 507 // get window icon 508 bool isSubGraph = graphObject?.graph?.isSubGraph ?? false; 509 Texture2D icon; 510 { 511 string theme = EditorGUIUtility.isProSkin ? "_dark" : "_light"; 512 if (isSubGraph) 513 icon = Resources.Load<Texture2D>("Icons/sg_subgraph_icon_gray" + theme); 514 else 515 icon = Resources.Load<Texture2D>("Icons/sg_graph_icon_gray" + theme); 516 } 517 518 titleContent = new GUIContent(title, icon); 519 } 520 521 void OnDestroy() 522 { 523 // Prompting the user if they want to close is mostly handled via the EditorWindow's system (hasUnsavedChanges). 524 // There's unfortunately a code path (Reload Window) that doesn't go through this path. The old logic is left 525 // here as a fallback to catch this. This has one edge case with "Reload Window" -> "Cancel" which will produce 526 // two shader graph windows: one unmodified (that the editor opens) and one modified (that we open below). 527 528 // we are closing the shadergraph window 529 MaterialGraphEditWindow newWindow = null; 530 if (!PromptSaveIfDirtyOnQuit()) 531 { 532 // user does not want to close the window. 533 // we can't stop the close from this code path though.. 534 // all we can do is open a new window and transfer our data to the new one to avoid losing it 535 // newWin = Instantiate<MaterialGraphEditWindow>(this); 536 newWindow = EditorWindow.CreateWindow<MaterialGraphEditWindow>(typeof(MaterialGraphEditWindow), typeof(SceneView)); 537 newWindow.Initialize(this); 538 } 539 else 540 { 541 // the window is closing for good.. cleanup undo history for the graph object 542 Undo.ClearUndo(graphObject); 543 } 544 545 // Discard any unsaved modification on the generated material 546 if (graphObject && graphObject.materialArtifact && EditorUtility.IsDirty(graphObject.materialArtifact)) 547 { 548 var material = new Material(AssetDatabase.LoadAssetAtPath<Shader>(AssetDatabase.GUIDToAssetPath(graphObject.AssetGuid))); 549 graphObject.materialArtifact.CopyPropertiesFromMaterial(material); 550 CoreUtils.Destroy(material); 551 } 552 553 graphObject = null; 554 graphEditorView = null; 555 556 // show new window if we have one 557 if (newWindow != null) 558 { 559 newWindow.Show(); 560 newWindow.Focus(); 561 } 562 } 563 564 public void PingAsset() 565 { 566 if (selectedGuid != null) 567 { 568 var path = AssetDatabase.GUIDToAssetPath(selectedGuid); 569 var asset = AssetDatabase.LoadAssetAtPath<Object>(path); 570 EditorGUIUtility.PingObject(asset); 571 } 572 } 573 574 public bool IsGraphAssetCheckedOut() 575 { 576 if (selectedGuid != null) 577 { 578 var path = AssetDatabase.GUIDToAssetPath(selectedGuid); 579 var asset = AssetDatabase.LoadAssetAtPath<Object>(path); 580 if (!AssetDatabase.IsOpenForEdit(asset, StatusQueryOptions.UseCachedIfPossible)) 581 return false; 582 583 return true; 584 } 585 586 return false; 587 } 588 589 public void CheckoutAsset() 590 { 591 if (selectedGuid != null) 592 { 593 var path = AssetDatabase.GUIDToAssetPath(selectedGuid); 594 var asset = AssetDatabase.LoadAssetAtPath<Object>(path); 595 Task task = Provider.Checkout(asset, CheckoutMode.Both); 596 task.Wait(); 597 } 598 } 599 600 // returns true if the asset was succesfully saved 601 public bool SaveAsset() 602 { 603 bool saved = false; 604 605 if (selectedGuid != null && graphObject != null) 606 { 607 var path = AssetDatabase.GUIDToAssetPath(selectedGuid); 608 if (string.IsNullOrEmpty(path) || graphObject == null) 609 return false; 610 611 if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(path, graphObject.graph.GetNodes<SubGraphNode>(), "Save")) 612 return false; 613 614 ShaderGraphAnalytics.SendShaderGraphEvent(selectedGuid, graphObject.graph); 615 616 var oldShader = AssetDatabase.LoadAssetAtPath<Shader>(path); 617 if (oldShader != null) 618 ShaderUtil.ClearShaderMessages(oldShader); 619 620 var newFileContents = FileUtilities.WriteShaderGraphToDisk(path, graphObject.graph); 621 if (newFileContents != null) 622 { 623 saved = true; 624 m_LastSerializedFileContents = newFileContents; 625 AssetDatabase.ImportAsset(path); 626 } 627 628 OnSaveGraph(path); 629 hasUnsavedChanges = false; 630 } 631 632 UpdateTitle(); 633 634 return saved; 635 } 636 637 void OnSaveGraph(string path) 638 { 639 if (GraphData.onSaveGraph == null) 640 return; 641 642 if (graphObject.graph.isSubGraph) 643 return; 644 645 var shader = AssetDatabase.LoadAssetAtPath<Shader>(path); 646 if (shader == null) 647 return; 648 649 foreach (var target in graphObject.graph.activeTargets) 650 { 651 GraphData.onSaveGraph(shader, target.saveContext); 652 } 653 } 654 655 public void SaveAs() 656 { 657 SaveAsImplementation(true); 658 } 659 660 // returns the asset path the file was saved to, or NULL if nothing was saved 661 string SaveAsImplementation(bool openWhenSaved) 662 { 663 string savedFilePath = null; 664 665 if (selectedGuid != null && graphObject?.graph != null) 666 { 667 var oldFilePath = AssetDatabase.GUIDToAssetPath(selectedGuid); 668 if (string.IsNullOrEmpty(oldFilePath) || graphObject == null) 669 return null; 670 671 // The asset's name needs to be removed from the path, otherwise SaveFilePanel assumes it's a folder 672 string oldDirectory = Path.GetDirectoryName(oldFilePath); 673 674 var extension = graphObject.graph.isSubGraph ? ShaderSubGraphImporter.Extension : ShaderGraphImporter.Extension; 675 var newFilePath = EditorUtility.SaveFilePanelInProject("Save Graph As...", Path.GetFileNameWithoutExtension(oldFilePath), extension, "", oldDirectory); 676 newFilePath = newFilePath.Replace(Application.dataPath, "Assets"); 677 678 if (newFilePath != oldFilePath) 679 { 680 if (!string.IsNullOrEmpty(newFilePath)) 681 { 682 // If the newPath already exists, we are overwriting an existing file, and could be creating recursions. Let's check. 683 if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(newFilePath, graphObject.graph.GetNodes<SubGraphNode>(), "Save As")) 684 return null; 685 686 bool success = (FileUtilities.WriteShaderGraphToDisk(newFilePath, graphObject.graph) != null); 687 AssetDatabase.ImportAsset(newFilePath); 688 if (success) 689 { 690 if (openWhenSaved) 691 ShaderGraphImporterEditor.ShowGraphEditWindow(newFilePath); 692 OnSaveGraph(newFilePath); 693 savedFilePath = newFilePath; 694 } 695 } 696 } 697 else 698 { 699 // saving to the current path 700 if (SaveAsset()) 701 { 702 graphObject.isDirty = false; 703 savedFilePath = oldFilePath; 704 } 705 } 706 } 707 return savedFilePath; 708 } 709 710 public void ToSubGraph() 711 { 712 var graphView = graphEditorView.graphView; 713 714 string path; 715 string sessionStateResult = SessionState.GetString(k_PrevSubGraphPathKey, k_PrevSubGraphPathDefaultValue); 716 string pathToOriginSG = Path.GetDirectoryName(AssetDatabase.GUIDToAssetPath(selectedGuid)); 717 718 if (!sessionStateResult.Equals(k_PrevSubGraphPathDefaultValue)) 719 { 720 path = sessionStateResult; 721 } 722 else 723 { 724 path = pathToOriginSG; 725 } 726 727 path = EditorUtility.SaveFilePanelInProject("Save Sub Graph", "New Shader Sub Graph", ShaderSubGraphImporter.Extension, "", path); 728 path = path.Replace(Application.dataPath, "Assets"); 729 730 // Friendly warning that the user is generating a subgraph that would overwrite the one they are currently working on. 731 if (AssetDatabase.AssetPathToGUID(path) == selectedGuid) 732 { 733 if (!EditorUtility.DisplayDialog("Overwrite Current Subgraph", "Do you want to overwrite this Sub Graph that you are currently working on? You cannot undo this operation.", "Yes", "Cancel")) 734 { 735 path = ""; 736 } 737 } 738 739 if (path.Length == 0) 740 return; 741 742 var nodes = graphView.selection.OfType<IShaderNodeView>().Where(x => !(x.node is PropertyNode || x.node is SubGraphOutputNode)).Select(x => x.node).Where(x => x.allowedInSubGraph).ToArray(); 743 744 // Convert To Subgraph could create recursive reference loops if the target path already exists 745 // Let's check for that here 746 if (!string.IsNullOrEmpty(path)) 747 { 748 if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(path, nodes.OfType<SubGraphNode>(), "Convert To SubGraph")) 749 return; 750 } 751 752 graphObject.RegisterCompleteObjectUndo("Convert To Subgraph"); 753 754 var bounds = Rect.MinMaxRect(float.PositiveInfinity, float.PositiveInfinity, float.NegativeInfinity, float.NegativeInfinity); 755 foreach (var node in nodes) 756 { 757 var center = node.drawState.position.center; 758 bounds = Rect.MinMaxRect( 759 Mathf.Min(bounds.xMin, center.x), 760 Mathf.Min(bounds.yMin, center.y), 761 Mathf.Max(bounds.xMax, center.x), 762 Mathf.Max(bounds.yMax, center.y)); 763 } 764 var middle = bounds.center; 765 bounds.center = Vector2.zero; 766 767 // Collect graph inputs 768 var graphInputs = graphView.selection.OfType<SGBlackboardField>().Select(x => x.userData as ShaderInput); 769 var categories = graphView.selection.OfType<SGBlackboardCategory>().Select(x => x.userData as CategoryData); 770 771 // Collect the property nodes and get the corresponding properties 772 var propertyNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is PropertyNode)).Select(x => ((PropertyNode)x.node).property); 773 var metaProperties = graphView.graph.properties.Where(x => propertyNodes.Contains(x)); 774 775 // Collect the keyword nodes and get the corresponding keywords 776 var keywordNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is KeywordNode)).Select(x => ((KeywordNode)x.node).keyword); 777 var dropdownNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is DropdownNode)).Select(x => ((DropdownNode)x.node).dropdown); 778 779 var metaKeywords = graphView.graph.keywords.Where(x => keywordNodes.Contains(x)); 780 var metaDropdowns = graphView.graph.dropdowns.Where(x => dropdownNodes.Contains(x)); 781 782 var copyPasteGraph = new CopyPasteGraph(graphView.selection.OfType<ShaderGroup>().Select(x => x.userData), 783 nodes, 784 graphView.selection.OfType<Edge>().Select(x => x.userData as Graphing.Edge), 785 graphInputs, 786 categories, 787 metaProperties, 788 metaKeywords, 789 metaDropdowns, 790 graphView.selection.OfType<StickyNote>().Select(x => x.userData), 791 true, 792 false); 793 794 // why do we serialize and deserialize only to make copies of everything in the steps below? 795 // is this just to clear out all non-serialized data? 796 var deserialized = CopyPasteGraph.FromJson(MultiJson.Serialize(copyPasteGraph), graphView.graph); 797 if (deserialized == null) 798 return; 799 800 var subGraph = new GraphData { isSubGraph = true, path = "Sub Graphs" }; 801 var subGraphOutputNode = new SubGraphOutputNode(); 802 { 803 var drawState = subGraphOutputNode.drawState; 804 drawState.position = new Rect(new Vector2(bounds.xMax + 200f, 0f), drawState.position.size); 805 subGraphOutputNode.drawState = drawState; 806 } 807 subGraph.AddNode(subGraphOutputNode); 808 subGraph.outputNode = subGraphOutputNode; 809 810 // Always copy deserialized keyword inputs 811 foreach (ShaderKeyword keyword in deserialized.metaKeywords) 812 { 813 var copiedInput = (ShaderKeyword)subGraph.AddCopyOfShaderInput(keyword); 814 815 // Update the keyword nodes that depends on the copied keyword 816 var dependentKeywordNodes = deserialized.GetNodes<KeywordNode>().Where(x => x.keyword == keyword); 817 foreach (var node in dependentKeywordNodes) 818 { 819 node.owner = graphView.graph; 820 node.keyword = copiedInput; 821 } 822 } 823 824 // Always copy deserialized dropdown inputs 825 foreach (ShaderDropdown dropdown in deserialized.metaDropdowns) 826 { 827 var copiedInput = (ShaderDropdown)subGraph.AddCopyOfShaderInput(dropdown); 828 829 // Update the dropdown nodes that depends on the copied dropdown 830 var dependentDropdownNodes = deserialized.GetNodes<DropdownNode>().Where(x => x.dropdown == dropdown); 831 foreach (var node in dependentDropdownNodes) 832 { 833 node.owner = graphView.graph; 834 node.dropdown = copiedInput; 835 } 836 } 837 838 foreach (GroupData groupData in deserialized.groups) 839 { 840 subGraph.CreateGroup(groupData); 841 } 842 843 foreach (var node in deserialized.GetNodes<AbstractMaterialNode>()) 844 { 845 var drawState = node.drawState; 846 drawState.position = new Rect(drawState.position.position - middle, drawState.position.size); 847 node.drawState = drawState; 848 849 // Checking if the group guid is also being copied. 850 // If not then nullify that guid 851 if (node.group != null && !subGraph.groups.Contains(node.group)) 852 { 853 node.group = null; 854 } 855 856 subGraph.AddNode(node); 857 } 858 859 foreach (var note in deserialized.stickyNotes) 860 { 861 if (note.group != null && !subGraph.groups.Contains(note.group)) 862 { 863 note.group = null; 864 } 865 866 subGraph.AddStickyNote(note); 867 } 868 869 // figure out what needs remapping 870 var externalOutputSlots = new List<Graphing.Edge>(); 871 var externalInputSlots = new List<Graphing.Edge>(); 872 var passthroughSlots = new List<Graphing.Edge>(); 873 foreach (var edge in deserialized.edges) 874 { 875 var outputSlot = edge.outputSlot; 876 var inputSlot = edge.inputSlot; 877 878 var outputSlotExistsInSubgraph = subGraph.ContainsNode(outputSlot.node); 879 var inputSlotExistsInSubgraph = subGraph.ContainsNode(inputSlot.node); 880 881 // pasting nice internal links! 882 if (outputSlotExistsInSubgraph && inputSlotExistsInSubgraph) 883 { 884 subGraph.Connect(outputSlot, inputSlot); 885 } 886 // one edge needs to go to outside world 887 else if (outputSlotExistsInSubgraph) 888 { 889 externalInputSlots.Add(edge); 890 } 891 else if (inputSlotExistsInSubgraph) 892 { 893 externalOutputSlots.Add(edge); 894 } 895 else 896 { 897 externalInputSlots.Add(edge); 898 externalOutputSlots.Add(edge); 899 passthroughSlots.Add(edge); 900 } 901 } 902 903 // Find the unique edges coming INTO the graph 904 var uniqueIncomingEdges = externalOutputSlots.GroupBy( 905 edge => edge.outputSlot, 906 edge => edge, 907 (key, edges) => new { slotRef = key, edges = edges.ToList() }); 908 909 var externalInputNeedingConnection = new List<KeyValuePair<IEdge, AbstractShaderProperty>>(); 910 911 var amountOfProps = uniqueIncomingEdges.Count(); 912 const int height = 40; 913 const int subtractHeight = 20; 914 var propPos = new Vector2(0, -((amountOfProps / 2) + height) - subtractHeight); 915 916 var passthroughSlotRefLookup = new Dictionary<SlotReference, SlotReference>(); 917 918 var passedInProperties = new Dictionary<AbstractShaderProperty, AbstractShaderProperty>(); 919 foreach (var group in uniqueIncomingEdges) 920 { 921 var sr = group.slotRef; 922 var fromNode = sr.node; 923 var fromSlot = sr.slot; 924 925 var materialGraph = graphObject.graph; 926 var fromProperty = fromNode is PropertyNode fromPropertyNode 927 ? materialGraph.properties.FirstOrDefault(p => p == fromPropertyNode.property) 928 : null; 929 930 AbstractShaderProperty prop; 931 if (fromProperty != null && passedInProperties.TryGetValue(fromProperty, out prop)) 932 { 933 } 934 else 935 { 936 switch (fromSlot.concreteValueType) 937 { 938 case ConcreteSlotValueType.Texture2D: 939 prop = new Texture2DShaderProperty(); 940 break; 941 case ConcreteSlotValueType.Texture2DArray: 942 prop = new Texture2DArrayShaderProperty(); 943 break; 944 case ConcreteSlotValueType.Texture3D: 945 prop = new Texture3DShaderProperty(); 946 break; 947 case ConcreteSlotValueType.Cubemap: 948 prop = new CubemapShaderProperty(); 949 break; 950 case ConcreteSlotValueType.Vector4: 951 prop = new Vector4ShaderProperty(); 952 break; 953 case ConcreteSlotValueType.Vector3: 954 prop = new Vector3ShaderProperty(); 955 break; 956 case ConcreteSlotValueType.Vector2: 957 prop = new Vector2ShaderProperty(); 958 break; 959 case ConcreteSlotValueType.Vector1: 960 prop = new Vector1ShaderProperty(); 961 break; 962 case ConcreteSlotValueType.Boolean: 963 prop = new BooleanShaderProperty(); 964 break; 965 case ConcreteSlotValueType.Matrix2: 966 prop = new Matrix2ShaderProperty(); 967 break; 968 case ConcreteSlotValueType.Matrix3: 969 prop = new Matrix3ShaderProperty(); 970 break; 971 case ConcreteSlotValueType.Matrix4: 972 prop = new Matrix4ShaderProperty(); 973 break; 974 case ConcreteSlotValueType.SamplerState: 975 prop = new SamplerStateShaderProperty(); 976 break; 977 case ConcreteSlotValueType.Gradient: 978 prop = new GradientShaderProperty(); 979 break; 980 case ConcreteSlotValueType.VirtualTexture: 981 prop = new VirtualTextureShaderProperty() 982 { 983 // also copy the VT settings over from the original property (if there is one) 984 value = (fromProperty as VirtualTextureShaderProperty)?.value ?? new SerializableVirtualTexture() 985 }; 986 break; 987 default: 988 throw new ArgumentOutOfRangeException(); 989 } 990 991 var propName = fromProperty != null 992 ? fromProperty.displayName 993 : fromSlot.concreteValueType.ToString(); 994 prop.SetDisplayNameAndSanitizeForGraph(subGraph, propName); 995 996 if (fromProperty?.useCustomSlotLabel ?? false) 997 { 998 prop.useCustomSlotLabel = true; 999 prop.customSlotLabel = fromProperty.customSlotLabel; 1000 } 1001 1002 subGraph.AddGraphInput(prop); 1003 if (fromProperty != null) 1004 { 1005 passedInProperties.Add(fromProperty, prop); 1006 } 1007 } 1008 1009 var propNode = new PropertyNode(); 1010 { 1011 var drawState = propNode.drawState; 1012 drawState.position = new Rect(new Vector2(bounds.xMin - 300f, 0f) + propPos, 1013 drawState.position.size); 1014 propPos += new Vector2(0, height); 1015 propNode.drawState = drawState; 1016 } 1017 subGraph.AddNode(propNode); 1018 propNode.property = prop; 1019 1020 1021 Vector2 avg = Vector2.zero; 1022 foreach (var edge in group.edges) 1023 { 1024 if (passthroughSlots.Contains(edge) && !passthroughSlotRefLookup.ContainsKey(sr)) 1025 { 1026 passthroughSlotRefLookup.Add(sr, new SlotReference(propNode, PropertyNode.OutputSlotId)); 1027 } 1028 else 1029 { 1030 subGraph.Connect( 1031 new SlotReference(propNode, PropertyNode.OutputSlotId), 1032 edge.inputSlot); 1033 1034 int i; 1035 var inputs = edge.inputSlot.node.GetInputSlots<MaterialSlot>().ToList(); 1036 1037 for (i = 0; i < inputs.Count; ++i) 1038 { 1039 if (inputs[i].slotReference.slotId == edge.inputSlot.slotId) 1040 { 1041 break; 1042 } 1043 } 1044 avg += new Vector2(edge.inputSlot.node.drawState.position.xMin, edge.inputSlot.node.drawState.position.center.y + 30f * i); 1045 } 1046 //we collapse input properties so dont add edges that are already being added 1047 if (!externalInputNeedingConnection.Any(x => x.Key.outputSlot.slot == edge.outputSlot.slot && x.Value == prop)) 1048 { 1049 externalInputNeedingConnection.Add(new KeyValuePair<IEdge, AbstractShaderProperty>(edge, prop)); 1050 } 1051 } 1052 avg /= group.edges.Count; 1053 var pos = avg - new Vector2(150f, 0f); 1054 propNode.drawState = new DrawState() 1055 { 1056 position = new Rect(pos, propNode.drawState.position.size), 1057 expanded = propNode.drawState.expanded 1058 }; 1059 } 1060 1061 var uniqueOutgoingEdges = externalInputSlots.GroupBy( 1062 edge => edge.outputSlot, 1063 edge => edge, 1064 (key, edges) => new { slot = key, edges = edges.ToList() }); 1065 1066 var externalOutputsNeedingConnection = new List<KeyValuePair<IEdge, IEdge>>(); 1067 foreach (var group in uniqueOutgoingEdges) 1068 { 1069 var outputNode = subGraph.outputNode as SubGraphOutputNode; 1070 1071 AbstractMaterialNode node = group.edges[0].outputSlot.node; 1072 MaterialSlot slot = node.FindSlot<MaterialSlot>(group.edges[0].outputSlot.slotId); 1073 var slotId = outputNode.AddSlot(slot.concreteValueType); 1074 1075 var inputSlotRef = new SlotReference(outputNode, slotId); 1076 1077 foreach (var edge in group.edges) 1078 { 1079 var newEdge = subGraph.Connect(passthroughSlotRefLookup.TryGetValue(edge.outputSlot, out SlotReference remap) ? remap : edge.outputSlot, inputSlotRef); 1080 externalOutputsNeedingConnection.Add(new KeyValuePair<IEdge, IEdge>(edge, newEdge)); 1081 } 1082 } 1083 1084 if (FileUtilities.WriteShaderGraphToDisk(path, subGraph) != null) 1085 AssetDatabase.ImportAsset(path); 1086 1087 // Store path for next time 1088 if (!pathToOriginSG.Equals(Path.GetDirectoryName(path))) 1089 { 1090 SessionState.SetString(k_PrevSubGraphPathKey, Path.GetDirectoryName(path)); 1091 } 1092 else 1093 { 1094 // Or continue to make it so that next time it will open up in the converted-from SG's directory 1095 SessionState.EraseString(k_PrevSubGraphPathKey); 1096 } 1097 1098 var loadedSubGraph = AssetDatabase.LoadAssetAtPath(path, typeof(SubGraphAsset)) as SubGraphAsset; 1099 if (loadedSubGraph == null) 1100 return; 1101 1102 var subGraphNode = new SubGraphNode(); 1103 var ds = subGraphNode.drawState; 1104 ds.position = new Rect(middle - new Vector2(100f, 150f), Vector2.zero); 1105 subGraphNode.drawState = ds; 1106 1107 // Add the subgraph into the group if the nodes was all in the same group group 1108 var firstNode = copyPasteGraph.GetNodes<AbstractMaterialNode>().FirstOrDefault(); 1109 if (firstNode != null && copyPasteGraph.GetNodes<AbstractMaterialNode>().All(x => x.group == firstNode.group)) 1110 { 1111 subGraphNode.group = firstNode.group; 1112 } 1113 1114 subGraphNode.asset = loadedSubGraph; 1115 graphObject.graph.AddNode(subGraphNode); 1116 1117 foreach (var edgeMap in externalInputNeedingConnection) 1118 { 1119 graphObject.graph.Connect(edgeMap.Key.outputSlot, new SlotReference(subGraphNode, edgeMap.Value.guid.GetHashCode())); 1120 } 1121 1122 foreach (var edgeMap in externalOutputsNeedingConnection) 1123 { 1124 graphObject.graph.Connect(new SlotReference(subGraphNode, edgeMap.Value.inputSlot.slotId), edgeMap.Key.inputSlot); 1125 } 1126 1127 graphObject.graph.RemoveElements( 1128 graphView.selection.OfType<IShaderNodeView>().Select(x => x.node).Where(x => !(x is PropertyNode || x is SubGraphOutputNode) && x.allowedInSubGraph).ToArray(), 1129 new IEdge[] { }, 1130 new GroupData[] { }, 1131 graphView.selection.OfType<StickyNote>().Select(x => x.userData).ToArray()); 1132 1133 List<GraphElement> moved = new List<GraphElement>(); 1134 foreach (var nodeView in graphView.selection.OfType<IShaderNodeView>()) 1135 { 1136 var node = nodeView.node; 1137 if (graphView.graph.removedNodes.Contains(node) || node is SubGraphOutputNode) 1138 { 1139 continue; 1140 } 1141 1142 var edges = graphView.graph.GetEdges(node); 1143 int numEdges = edges.Count(); 1144 if (numEdges == 0) 1145 { 1146 graphView.graph.RemoveNode(node); 1147 } 1148 else if (numEdges == 1 && edges.First().inputSlot.node != node) //its an output edge 1149 { 1150 var edge = edges.First(); 1151 int i; 1152 var inputs = edge.inputSlot.node.GetInputSlots<MaterialSlot>().ToList(); 1153 for (i = 0; i < inputs.Count; ++i) 1154 { 1155 if (inputs[i].slotReference.slotId == edge.inputSlot.slotId) 1156 { 1157 break; 1158 } 1159 } 1160 node.drawState = new DrawState() 1161 { 1162 position = new Rect(new Vector2(edge.inputSlot.node.drawState.position.xMin, edge.inputSlot.node.drawState.position.center.y) - new Vector2(150f, -30f * i), node.drawState.position.size), 1163 expanded = node.drawState.expanded 1164 }; 1165 (nodeView as GraphElement).SetPosition(node.drawState.position); 1166 } 1167 } 1168 graphObject.graph.ValidateGraph(); 1169 } 1170 1171 public void Initialize(MaterialGraphEditWindow other) 1172 { 1173 // create a new window that copies the entire editor state of an existing window 1174 // this function is used to "reopen" an editor window that is closing, but where the user has canceled the close 1175 // for example, if the graph of a closing window was dirty, but could not be saved 1176 try 1177 { 1178 selectedGuid = other.selectedGuid; 1179 1180 graphObject = CreateInstance<GraphObject>(); 1181 graphObject.hideFlags = HideFlags.HideAndDontSave; 1182 graphObject.graph = other.graphObject.graph; 1183 1184 graphObject.graph.messageManager = this.messageManager; 1185 1186 UpdateTitle(); 1187 1188 Repaint(); 1189 } 1190 catch (Exception e) 1191 { 1192 Debug.LogException(e); 1193 m_HasError = true; 1194 m_GraphEditorView = null; 1195 graphObject = null; 1196 throw; 1197 } 1198 } 1199 1200 private static readonly ProfilerMarker GraphLoadMarker = new ProfilerMarker("GraphLoad"); 1201 private static readonly ProfilerMarker CreateGraphEditorViewMarker = new ProfilerMarker("CreateGraphEditorView"); 1202 public void Initialize(string assetGuid) 1203 { 1204 try 1205 { 1206 m_ColorSpace = PlayerSettings.colorSpace; 1207 m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline; 1208 1209 var asset = AssetDatabase.LoadAssetAtPath<Object>(AssetDatabase.GUIDToAssetPath(assetGuid)); 1210 if (asset == null) 1211 return; 1212 1213 if (!EditorUtility.IsPersistent(asset)) 1214 return; 1215 1216 if (selectedGuid == assetGuid) 1217 return; 1218 1219 1220 var path = AssetDatabase.GetAssetPath(asset); 1221 var extension = Path.GetExtension(path); 1222 if (extension == null) 1223 return; 1224 // Path.GetExtension returns the extension prefixed with ".", so we remove it. We force lower case such that 1225 // the comparison will be case-insensitive. 1226 extension = extension.Substring(1).ToLowerInvariant(); 1227 bool isSubGraph; 1228 switch (extension) 1229 { 1230 case ShaderGraphImporter.Extension: 1231 isSubGraph = false; 1232 break; 1233 case ShaderSubGraphImporter.Extension: 1234 isSubGraph = true; 1235 break; 1236 default: 1237 return; 1238 } 1239 1240 selectedGuid = assetGuid; 1241 string graphName = Path.GetFileNameWithoutExtension(path); 1242 1243 using (GraphLoadMarker.Auto()) 1244 { 1245 m_LastSerializedFileContents = File.ReadAllText(path, Encoding.UTF8); 1246 graphObject = CreateInstance<GraphObject>(); 1247 graphObject.hideFlags = HideFlags.HideAndDontSave; 1248 graphObject.graph = new GraphData 1249 { 1250 assetGuid = assetGuid, 1251 isSubGraph = isSubGraph, 1252 messageManager = messageManager 1253 }; 1254 MultiJson.Deserialize(graphObject.graph, m_LastSerializedFileContents); 1255 graphObject.graph.OnEnable(); 1256 graphObject.graph.ValidateGraph(); 1257 } 1258 1259 using (CreateGraphEditorViewMarker.Auto()) 1260 { 1261 graphEditorView = new GraphEditorView(this, m_GraphObject.graph, messageManager, graphName) 1262 { 1263 viewDataKey = selectedGuid, 1264 }; 1265 } 1266 1267 UpdateTitle(); 1268 1269 Repaint(); 1270 } 1271 catch (Exception) 1272 { 1273 m_HasError = true; 1274 m_GraphEditorView = null; 1275 graphObject = null; 1276 throw; 1277 } 1278 } 1279 1280 // returns contents of the asset file, or null if any exception occurred 1281 private string ReadAssetFile() 1282 { 1283 var filePath = AssetDatabase.GUIDToAssetPath(selectedGuid); 1284 return FileUtilities.SafeReadAllText(filePath); 1285 } 1286 1287 // returns true when the user is OK with closing the window or application (either they've saved dirty content, or are ok with losing it) 1288 // returns false when the user wants to cancel closing the window or application 1289 internal bool PromptSaveIfDirtyOnQuit() 1290 { 1291 // only bother unless we've actually got data to preserve 1292 if (graphObject?.graph != null) 1293 { 1294 // if the asset has been deleted, ask the user what to do 1295 if (!AssetFileExists()) 1296 return DisplayDeletedFromDiskDialog(false); 1297 1298 // If there are unsaved modifications, ask the user what to do. 1299 // If the editor has already handled this check we'll no longer have unsaved changes 1300 // (either they saved or they discarded, both of which will set hasUnsavedChanges to false). 1301 if (hasUnsavedChanges) 1302 { 1303 int option = EditorUtility.DisplayDialogComplex( 1304 "Shader Graph Has Been Modified", 1305 GetSaveChangesMessage(), 1306 "Save", "Cancel", "Discard Changes"); 1307 1308 if (option == 0) // save 1309 { 1310 return SaveAsset(); 1311 } 1312 else if (option == 1) // cancel (or escape/close dialog) 1313 { 1314 return false; 1315 } 1316 else if (option == 2) // discard 1317 { 1318 return true; 1319 } 1320 } 1321 } 1322 return true; 1323 } 1324 1325 private string GetSaveChangesMessage() 1326 { 1327 return "Do you want to save the changes you made in the Shader Graph?\n\n" + 1328 AssetDatabase.GUIDToAssetPath(selectedGuid) + 1329 "\n\nYour changes will be lost if you don't save them."; 1330 } 1331 1332 public override void SaveChanges() 1333 { 1334 base.SaveChanges(); 1335 SaveAsset(); 1336 } 1337 1338 void OnGeometryChanged(GeometryChangedEvent evt) 1339 { 1340 if (graphEditorView == null) 1341 return; 1342 1343 // this callback is only so we can run post-layout behaviors after the graph loads for the first time 1344 // we immediately unregister it so it doesn't get called again 1345 graphEditorView.UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged); 1346 if (m_FrameAllAfterLayout) 1347 graphEditorView.graphView.FrameAll(); 1348 m_FrameAllAfterLayout = false; 1349 } 1350 1351 private void OnBecameVisible() 1352 { 1353 isVisible = true; 1354 } 1355 1356 private void OnBecameInvisible() 1357 { 1358 isVisible = false; 1359 } 1360 } 1361}