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}