A game about forced loneliness, made by TACStudios
1using System; 2using System.Collections.Generic; 3using System.Linq; 4using UnityEditor; 5using UnityEngine; 6using UnityObject = UnityEngine.Object; 7 8namespace Unity.VisualScripting 9{ 10 [Canvas(typeof(FlowGraph))] 11 public sealed class FlowCanvas : VisualScriptingCanvas<FlowGraph> 12 { 13 public FlowCanvas(FlowGraph graph) : base(graph) { } 14 15 16 #region Clipboard 17 18 public override void ShrinkCopyGroup(HashSet<IGraphElement> copyGroup) 19 { 20 copyGroup.RemoveWhere(element => 21 { 22 if (element is IUnitConnection) 23 { 24 var connection = (IUnitConnection)element; 25 26 if (!copyGroup.Contains(connection.source.unit) || 27 !copyGroup.Contains(connection.destination.unit)) 28 { 29 return true; 30 } 31 } 32 33 return false; 34 }); 35 } 36 37 #endregion 38 39 40 #region Window 41 42 public override void OnToolbarGUI() 43 { 44 showRelations = GUILayout.Toggle(showRelations, "Relations", LudiqStyles.toolbarButton); 45 46 EditorGUI.BeginChangeCheck(); 47 48 BoltFlow.Configuration.showConnectionValues = GUILayout.Toggle(BoltFlow.Configuration.showConnectionValues, "Values", LudiqStyles.toolbarButton); 49 50 BoltCore.Configuration.dimInactiveNodes = GUILayout.Toggle(BoltCore.Configuration.dimInactiveNodes, "Dim", LudiqStyles.toolbarButton); 51 52 if (EditorGUI.EndChangeCheck()) 53 { 54 BoltFlow.Configuration.Save(); 55 56 BoltCore.Configuration.Save(); 57 } 58 59 base.OnToolbarGUI(); 60 } 61 62 #endregion 63 64 65 #region View 66 67 protected override bool shouldEdgePan => base.shouldEdgePan || isCreatingConnection; 68 69 public const float inspectorZoomThreshold = 0.7f; 70 71 #endregion 72 73 74 #region Lifecycle 75 76 public override void Close() 77 { 78 base.Close(); 79 80 CancelConnection(); 81 } 82 83 protected override void HandleHighPriorityInput() 84 { 85 if (isCreatingConnection) 86 { 87 if (e.IsMouseDown(MouseButton.Left)) 88 { 89 connectionEnd = mousePosition; 90 NewUnitContextual(); 91 e.Use(); 92 } 93 else if (e.IsFree(EventType.KeyDown) && e.keyCode == KeyCode.Escape) 94 { 95 CancelConnection(); 96 e.Use(); 97 } 98 } 99 100 base.HandleHighPriorityInput(); 101 } 102 103 private void CompleteContextualConnection(IUnitPort source, IUnitPort destination) 104 { 105 source.ValidlyConnectTo(destination); 106 Cache(); 107 var unitPosition = this.Widget<IUnitWidget>(destination.unit).position.position; 108 var portPosition = this.Widget<IUnitPortWidget>(destination).handlePosition.center.PixelPerfect(); 109 var offset = portPosition - unitPosition; 110 destination.unit.position -= offset; 111 this.Widget(destination.unit).Reposition(); 112 connectionSource = null; 113 GUI.changed = true; 114 } 115 116 public void NewUnitContextual() 117 { 118 var filter = UnitOptionFilter.Any; 119 filter.GraphHashCode = graph.GetHashCode(); 120 121 if (connectionSource is ValueInput) 122 { 123 var valueInput = (ValueInput)connectionSource; 124 filter.CompatibleOutputType = valueInput.type; 125 filter.Expose = false; 126 filter.NoConnection = false; 127 NewUnit(mousePosition, GetNewUnitOptions(filter), (unit) => CompleteContextualConnection(valueInput, unit.CompatibleValueOutput(valueInput.type))); 128 } 129 else if (connectionSource is ValueOutput) 130 { 131 var valueOutput = (ValueOutput)connectionSource; 132 filter.CompatibleInputType = valueOutput.type; 133 filter.NoConnection = false; 134 NewUnit(mousePosition, GetNewUnitOptions(filter), (unit) => CompleteContextualConnection(valueOutput, unit.CompatibleValueInput(valueOutput.type))); 135 } 136 else if (connectionSource is ControlInput) 137 { 138 var controlInput = (ControlInput)connectionSource; 139 filter.NoControlOutput = false; 140 filter.NoConnection = false; 141 NewUnit(mousePosition, GetNewUnitOptions(filter), (unit) => CompleteContextualConnection(controlInput, unit.controlOutputs.First())); 142 } 143 else if (connectionSource is ControlOutput) 144 { 145 var controlOutput = (ControlOutput)connectionSource; 146 filter.NoControlInput = false; 147 filter.NoConnection = false; 148 NewUnit(mousePosition, GetNewUnitOptions(filter), (unit) => CompleteContextualConnection(controlOutput, unit.controlInputs.First())); 149 } 150 } 151 152 #endregion 153 154 155 #region Context 156 157 protected override void OnContext() 158 { 159 if (isCreatingConnection) 160 { 161 CancelConnection(); 162 } 163 else 164 { 165 // Checking for Alt seems to lose focus, for some reason maybe 166 // unrelated to Bolt. Shift or other modifiers seem to work though. 167 if (base.GetContextOptions().Any() && (!BoltFlow.Configuration.skipContextMenu || e.shift)) 168 { 169 base.OnContext(); 170 } 171 else 172 { 173 NewUnit(mousePosition); 174 } 175 } 176 } 177 178 protected override IEnumerable<DropdownOption> GetContextOptions() 179 { 180 yield return new DropdownOption((Action<Vector2>)(NewUnit), "Add Node..."); 181 yield return new DropdownOption((Action<Vector2>)(NewSticky), "Create Sticky Note"); 182 foreach (var baseOption in base.GetContextOptions()) 183 { 184 yield return baseOption; 185 } 186 } 187 188 public void AddUnit(IUnit unit, Vector2 position) 189 { 190 UndoUtility.RecordEditedObject("Create Node"); 191 unit.guid = Guid.NewGuid(); 192 unit.position = position.PixelPerfect(); 193 graph.units.Add(unit); 194 selection.Select(unit); 195 GUI.changed = true; 196 } 197 198 private UnitOptionTree GetNewUnitOptions(UnitOptionFilter filter) 199 { 200 var options = new UnitOptionTree(new GUIContent("Node")); 201 202 options.filter = filter; 203 options.reference = reference; 204 205 if (filter.CompatibleOutputType == typeof(object)) 206 { 207 options.surfaceCommonTypeLiterals = true; 208 } 209 210 return options; 211 } 212 213 private void NewSticky(Vector2 position) 214 { 215 UndoUtility.RecordEditedObject("Create Sticky Note"); 216 var stickyNote = new StickyNote() { position = new Rect(position, new Vector2(100, 100)) }; 217 graph.elements.Add(stickyNote); 218 selection.Select(stickyNote); 219 GUI.changed = true; 220 } 221 222 private void NewUnit(Vector2 position) 223 { 224 var filter = UnitOptionFilter.Any; 225 filter.GraphHashCode = graph.GetHashCode(); 226 NewUnit(position, GetNewUnitOptions(filter)); 227 } 228 229 private void NewUnit(Vector2 unitPosition, UnitOptionTree options, Action<IUnit> then = null) 230 { 231 delayCall += () => 232 { 233 var activatorPosition = new Rect(e.mousePosition, new Vector2(200, 1)); 234 235 var context = this.context; 236 237 LudiqGUI.FuzzyDropdown 238 ( 239 activatorPosition, 240 options, 241 null, 242 delegate (object _option) 243 { 244 context.BeginEdit(); 245 if (_option is IUnitOption) 246 { 247 var option = (IUnitOption)_option; 248 var unit = option.InstantiateUnit(); 249 AddUnit(unit, unitPosition); 250 option.PreconfigureUnit(unit); 251 then?.Invoke(unit); 252 GUI.changed = true; 253 } 254 else 255 { 256 if ((Type)_option == typeof(StickyNote)) 257 { 258 NewSticky(unitPosition); 259 } 260 } 261 262 context.EndEdit(); 263 } 264 ); 265 }; 266 } 267 268 #endregion 269 270 271 #region Drag & Drop 272 273 private bool CanDetermineDraggedInput(UnityObject uo) 274 { 275 if (uo.IsSceneBound()) 276 { 277 if (reference.self == uo.GameObject()) 278 { 279 // Because we'll be able to assign it to Self 280 return true; 281 } 282 283 if (reference.serializedObject.IsSceneBound()) 284 { 285 // Because we'll be able to use a direct scene reference 286 return true; 287 } 288 289 return false; 290 } 291 else 292 { 293 return true; 294 } 295 } 296 297 public override bool AcceptsDragAndDrop() 298 { 299 if (DragAndDropUtility.Is<ScriptGraphAsset>()) 300 { 301 return FlowDragAndDropUtility.AcceptsScript(graph); 302 } 303 304 return DragAndDropUtility.Is<UnityObject>() && !DragAndDropUtility.Is<IMacro>() && CanDetermineDraggedInput(DragAndDropUtility.Get<UnityObject>()) 305 || EditorVariablesUtility.isDraggingVariable; 306 } 307 308 public override void PerformDragAndDrop() 309 { 310 if (DragAndDropUtility.Is<ScriptGraphAsset>()) 311 { 312 var flowMacro = DragAndDropUtility.Get<ScriptGraphAsset>(); 313 var superUnit = new SubgraphUnit(flowMacro); 314 AddUnit(superUnit, DragAndDropUtility.position); 315 } 316 else if (DragAndDropUtility.Is<UnityObject>()) 317 { 318 var uo = DragAndDropUtility.Get<UnityObject>(); 319 var type = uo.GetType(); 320 var filter = UnitOptionFilter.Any; 321 filter.Literals = false; 322 filter.Expose = false; 323 var options = GetNewUnitOptions(filter); 324 325 var root = new List<object>(); 326 327 if (!uo.IsSceneBound() || reference.serializedObject.IsSceneBound()) 328 { 329 if (uo == reference.self) 330 { 331 root.Add(new UnitOption<This>(new This())); 332 } 333 334 root.Add(new LiteralOption(new Literal(type, uo))); 335 } 336 337 if (uo is MonoScript script) 338 { 339 var scriptType = script.GetClass(); 340 341 if (scriptType != null) 342 { 343 root.Add(scriptType); 344 } 345 } 346 else 347 { 348 root.Add(type); 349 } 350 351 if (uo is GameObject) 352 { 353 root.AddRange(uo.GetComponents<Component>().Select(c => c.GetType())); 354 } 355 356 options.rootOverride = root.ToArray(); 357 358 NewUnit(DragAndDropUtility.position, options, (unit) => 359 { 360 // Try to assign a correct input 361 var compatibleInput = unit.CompatibleValueInput(type); 362 363 if (compatibleInput == null) 364 { 365 return; 366 } 367 368 if (uo.IsSceneBound()) 369 { 370 if (reference.self == uo.GameObject()) 371 { 372 // The component is owned by the same game object as the graph. 373 374 if (compatibleInput.nullMeansSelf) 375 { 376 compatibleInput.SetDefaultValue(null); 377 } 378 else 379 { 380 var self = new This(); 381 self.position = unit.position + new Vector2(-150, 19); 382 graph.units.Add(self); 383 self.self.ConnectToValid(compatibleInput); 384 } 385 } 386 else if (reference.serializedObject.IsSceneBound()) 387 { 388 // The component is from another object from the same scene 389 compatibleInput.SetDefaultValue(uo.ConvertTo(compatibleInput.type)); 390 } 391 else 392 { 393 throw new NotSupportedException("Cannot determine compatible input from dragged Unity object."); 394 } 395 } 396 else 397 { 398 compatibleInput.SetDefaultValue(uo.ConvertTo(compatibleInput.type)); 399 } 400 }); 401 } 402 else if (EditorVariablesUtility.isDraggingVariable) 403 { 404 var kind = EditorVariablesUtility.kind; 405 var declaration = EditorVariablesUtility.declaration; 406 407 UnifiedVariableUnit unit; 408 409 if (e.alt) 410 { 411 unit = new SetVariable(); 412 } 413 else if (e.shift) 414 { 415 unit = new IsVariableDefined(); 416 } 417 else 418 { 419 unit = new GetVariable(); 420 } 421 422 unit.kind = kind; 423 AddUnit(unit, DragAndDropUtility.position); 424 unit.name.SetDefaultValue(declaration.name); 425 } 426 } 427 428 public override void DrawDragAndDropPreview() 429 { 430 if (DragAndDropUtility.Is<ScriptGraphAsset>()) 431 { 432 GraphGUI.DrawDragAndDropPreviewLabel(DragAndDropUtility.offsetedPosition, DragAndDropUtility.Get<ScriptGraphAsset>().name, typeof(ScriptGraphAsset).Icon()); 433 } 434 else if (DragAndDropUtility.Is<GameObject>()) 435 { 436 var gameObject = DragAndDropUtility.Get<GameObject>(); 437 GraphGUI.DrawDragAndDropPreviewLabel(DragAndDropUtility.offsetedPosition, gameObject.name + "...", gameObject.Icon()); 438 } 439 else if (DragAndDropUtility.Is<UnityObject>()) 440 { 441 var obj = DragAndDropUtility.Get<UnityObject>(); 442 var type = obj.GetType(); 443 GraphGUI.DrawDragAndDropPreviewLabel(DragAndDropUtility.offsetedPosition, type.HumanName() + "...", type.Icon()); 444 } 445 else if (EditorVariablesUtility.isDraggingVariable) 446 { 447 var kind = EditorVariablesUtility.kind; 448 var name = EditorVariablesUtility.declaration.name; 449 450 string label; 451 452 if (e.alt) 453 { 454 label = $"Set {name}"; 455 } 456 else if (e.shift) 457 { 458 label = $"Check if {name} is defined"; 459 } 460 else 461 { 462 label = $"Get {name}"; 463 } 464 465 GraphGUI.DrawDragAndDropPreviewLabel(DragAndDropUtility.offsetedPosition, label, BoltCore.Icons.VariableKind(kind)); 466 } 467 } 468 469 #endregion 470 471 472 #region Drawing 473 474 public bool showRelations { get; set; } 475 476 #endregion 477 478 479 #region Connection Creation 480 481 public IUnitPort connectionSource { get; set; } 482 483 public Vector2 connectionEnd { get; set; } 484 485 public bool isCreatingConnection => connectionSource != null && 486 connectionSource.unit != null; // Make sure the port didn't get destroyed: https://support.ludiq.io/communities/5/topics/4034-x 487 488 public void CancelConnection() 489 { 490 connectionSource = null; 491 } 492 493 #endregion 494 } 495}