A game about forced loneliness, made by TACStudios
1using System.Collections.Generic;
2using UnityEditor;
3using UnityEngine;
4
5namespace Unity.VisualScripting
6{
7 public abstract class StateTransitionWidget<TStateTransition> : NodeWidget<StateCanvas, TStateTransition>, IStateTransitionWidget
8 where TStateTransition : class, IStateTransition
9 {
10 protected StateTransitionWidget(StateCanvas canvas, TStateTransition transition) : base(canvas, transition) { }
11
12
13 #region Model
14
15 protected TStateTransition transition => element;
16
17 protected IStateTransitionDebugData transitionDebugData => GetDebugData<IStateTransitionDebugData>();
18
19 private StateTransitionDescription description;
20
21 private StateTransitionAnalysis analysis => transition.Analysis<StateTransitionAnalysis>(context);
22
23 protected override void CacheDescription()
24 {
25 description = transition.Description<StateTransitionDescription>();
26
27 label.text = description.label;
28 label.image = description.icon?[IconSize.Small];
29 label.tooltip = description.tooltip;
30
31 if (!revealLabel)
32 {
33 label.tooltip = label.text + ": " + label.tooltip;
34 }
35
36 Reposition();
37 }
38
39 #endregion
40
41 #region Lifecycle
42
43 public override void BeforeFrame()
44 {
45 base.BeforeFrame();
46
47 if (showDroplets)
48 {
49 GraphGUI.UpdateDroplets(canvas, droplets, transitionDebugData.lastBranchFrame, ref lastBranchTime, ref dropTime);
50 }
51
52 if (currentInnerWidth != targetInnerWidth)
53 {
54 Reposition();
55 }
56 }
57
58 #endregion
59
60
61 #region Contents
62
63 private GUIContent label { get; } = new GUIContent();
64
65 #endregion
66
67
68 #region Positioning
69
70 private readonly List<IStateTransition> siblingStateTransitions = new List<IStateTransition>();
71
72 private Rect sourcePosition;
73
74 private Rect destinationPosition;
75
76 private Edge sourceEdge;
77
78 private Edge entryEdge;
79
80 private Edge exitEdge;
81
82 private Edge destinationEdge;
83
84 private Vector2 sourceEdgeCenter;
85
86 private Vector2 entryEdgeCenter;
87
88 private Vector2 exitEdgeCenter;
89
90 private Vector2 destinationEdgeCenter;
91
92 private Vector2 middle;
93
94 private Rect _position;
95
96 private Rect _clippingPosition;
97
98 private float targetInnerWidth;
99
100 private float currentInnerWidth;
101
102 private bool revealInitialized;
103
104 private float minBend
105 {
106 get
107 {
108 if (transition.source != transition.destination)
109 {
110 return 15;
111 }
112 else
113 {
114 return (middle.y - canvas.Widget(transition.source).position.center.y) / 2;
115 }
116 }
117 }
118
119 private float relativeBend => 1 / 4f;
120
121 Edge IStateTransitionWidget.sourceEdge => sourceEdge;
122
123 public override IEnumerable<IWidget> positionDependencies
124 {
125 get
126 {
127 yield return canvas.Widget(transition.source);
128 yield return canvas.Widget(transition.destination);
129 }
130 }
131
132 public override IEnumerable<IWidget> positionDependers
133 {
134 get
135 {
136 // Return all sibling transitions. This is an asymetrical dependency / depender
137 // relation (because the siblings are not included in the dependers) to force
138 // repositioning of siblings while avoiding stack overflow.
139
140 foreach (var graphTransition in canvas.graph.transitions)
141 {
142 var current = transition == graphTransition;
143
144 var analog =
145 transition.source == graphTransition.source &&
146 transition.destination == graphTransition.destination;
147
148 var inverted =
149 transition.source == graphTransition.destination &&
150 transition.destination == graphTransition.source;
151
152 if (!current && (analog || inverted))
153 {
154 var widget = canvas.Widget(graphTransition);
155
156 if (widget.isPositionValid) // Avoid stack overflow
157 {
158 yield return widget;
159 }
160 }
161 }
162 }
163 }
164
165 public Rect iconPosition { get; private set; }
166
167 public Rect clipPosition { get; private set; }
168
169 public Rect labelInnerPosition { get; private set; }
170
171 public override Rect position
172 {
173 get { return _position; }
174 set { }
175 }
176
177 public override Rect clippingPosition => _clippingPosition;
178
179 public override void CachePositionFirstPass()
180 {
181 // Calculate the size immediately, because other transitions will rely on it for positioning
182
183 targetInnerWidth = Styles.eventIcon.fixedWidth;
184
185 var labelWidth = Styles.label.CalcSize(label).x;
186 var labelHeight = EditorGUIUtility.singleLineHeight;
187
188 if (revealLabel)
189 {
190 targetInnerWidth += Styles.spaceAroundIcon;
191 targetInnerWidth += labelWidth;
192 }
193
194 if (!revealInitialized)
195 {
196 currentInnerWidth = targetInnerWidth;
197 revealInitialized = true;
198 }
199
200 currentInnerWidth = Mathf.Lerp(currentInnerWidth, targetInnerWidth, canvas.repaintDeltaTime * Styles.revealSpeed);
201
202 if (Mathf.Abs(targetInnerWidth - currentInnerWidth) < 1)
203 {
204 currentInnerWidth = targetInnerWidth;
205 }
206
207 var innerWidth = currentInnerWidth;
208 var innerHeight = labelHeight;
209
210 var edgeSize = InnerToEdgePosition(new Rect(0, 0, innerWidth, innerHeight)).size;
211 var edgeWidth = edgeSize.x;
212 var edgeHeight = edgeSize.y;
213
214 _position.width = edgeWidth;
215 _position.height = edgeHeight;
216 }
217
218 public override void CachePosition()
219 {
220 var innerWidth = innerPosition.width;
221 var innerHeight = innerPosition.height;
222 var edgeWidth = edgePosition.width;
223 var edgeHeight = edgePosition.height;
224 var labelWidth = Styles.label.CalcSize(label).x;
225 var labelHeight = EditorGUIUtility.singleLineHeight;
226
227 sourcePosition = canvas.Widget(transition.source).position;
228 destinationPosition = canvas.Widget(transition.destination).position;
229
230 Vector2 sourceClosestPoint;
231 Vector2 destinationClosestPoint;
232 LudiqGUIUtility.ClosestPoints(sourcePosition, destinationPosition, out sourceClosestPoint, out destinationClosestPoint);
233
234 if (transition.destination != transition.source)
235 {
236 GraphGUI.GetConnectionEdge
237 (
238 sourceClosestPoint,
239 destinationClosestPoint,
240 out sourceEdge,
241 out destinationEdge
242 );
243 }
244 else
245 {
246 sourceEdge = Edge.Right;
247 destinationEdge = Edge.Left;
248 }
249
250 sourceEdgeCenter = sourcePosition.GetEdgeCenter(sourceEdge);
251 destinationEdgeCenter = destinationPosition.GetEdgeCenter(destinationEdge);
252
253 siblingStateTransitions.Clear();
254
255 var siblingIndex = 0;
256
257 // Assign one common axis for transition for all siblings,
258 // regardless of their inversion. The axis is arbitrarily
259 // chosen as the axis for the first transition.
260 var assignedTransitionAxis = false;
261 var transitionAxis = Vector2.zero;
262
263 foreach (var graphTransition in canvas.graph.transitions)
264 {
265 var current = transition == graphTransition;
266
267 var analog =
268 transition.source == graphTransition.source &&
269 transition.destination == graphTransition.destination;
270
271 var inverted =
272 transition.source == graphTransition.destination &&
273 transition.destination == graphTransition.source;
274
275 if (current)
276 {
277 siblingIndex = siblingStateTransitions.Count;
278 }
279
280 if (current || analog || inverted)
281 {
282 if (!assignedTransitionAxis)
283 {
284 var siblingStateTransitionDrawer = canvas.Widget<IStateTransitionWidget>(graphTransition);
285
286 transitionAxis = siblingStateTransitionDrawer.sourceEdge.Normal();
287
288 assignedTransitionAxis = true;
289 }
290
291 siblingStateTransitions.Add(graphTransition);
292 }
293 }
294
295 // Fix the edge case where the source and destination perfectly overlap
296
297 if (transitionAxis == Vector2.zero)
298 {
299 transitionAxis = Vector2.right;
300 }
301
302 // Calculate the spread axis and origin for the set of siblings
303
304 var spreadAxis = transitionAxis.Perpendicular1().Abs();
305 var spreadOrigin = (sourceEdgeCenter + destinationEdgeCenter) / 2;
306
307 if (transition.source == transition.destination)
308 {
309 spreadAxis = Vector2.up;
310 spreadOrigin = sourcePosition.GetEdgeCenter(Edge.Bottom) - Vector2.down * 10;
311 }
312
313 if (BoltCore.Configuration.developerMode && BoltCore.Configuration.debug)
314 {
315 Handles.BeginGUI();
316 Handles.color = Color.yellow;
317 Handles.DrawLine(spreadOrigin + spreadAxis * -1000, spreadOrigin + spreadAxis * 1000);
318 Handles.EndGUI();
319 }
320
321 // Calculate the offset of the current sibling by iterating over its predecessors
322
323 var spreadOffset = 0f;
324 var previousSpreadSize = 0f;
325
326 for (var i = 0; i <= siblingIndex; i++)
327 {
328 var siblingSize = canvas.Widget<IStateTransitionWidget>(siblingStateTransitions[i]).outerPosition.size;
329 var siblingSizeProjection = GraphGUI.SizeProjection(siblingSize, spreadOrigin, spreadAxis);
330 spreadOffset += previousSpreadSize / 2 + siblingSizeProjection / 2;
331 previousSpreadSize = siblingSizeProjection;
332 }
333
334 if (transition.source != transition.destination)
335 {
336 // Calculate the total spread size to center the sibling set
337
338 var totalSpreadSize = 0f;
339
340 for (var i = 0; i < siblingStateTransitions.Count; i++)
341 {
342 var siblingSize = canvas.Widget<IStateTransitionWidget>(siblingStateTransitions[i]).outerPosition.size;
343 var siblingSizeProjection = GraphGUI.SizeProjection(siblingSize, spreadOrigin, spreadAxis);
344 totalSpreadSize += siblingSizeProjection;
345 }
346
347 spreadOffset -= totalSpreadSize / 2;
348 }
349
350 // Finally, calculate the positions
351
352 middle = spreadOrigin + spreadOffset * spreadAxis;
353
354 var edgeX = middle.x - edgeWidth / 2;
355 var edgeY = middle.y - edgeHeight / 2;
356
357 _position = new Rect
358 (
359 edgeX,
360 edgeY,
361 edgeWidth,
362 edgeHeight
363 ).PixelPerfect();
364
365 var innerX = innerPosition.x;
366 var innerY = innerPosition.y;
367
368 _clippingPosition = _position.Encompass(sourceEdgeCenter).Encompass(destinationEdgeCenter);
369
370 if (transition.source != transition.destination)
371 {
372 entryEdge = destinationEdge;
373 exitEdge = sourceEdge;
374 }
375 else
376 {
377 entryEdge = sourceEdge;
378 exitEdge = destinationEdge;
379 }
380
381 entryEdgeCenter = edgePosition.GetEdgeCenter(entryEdge);
382 exitEdgeCenter = edgePosition.GetEdgeCenter(exitEdge);
383
384 var x = innerX;
385
386 iconPosition = new Rect
387 (
388 x,
389 innerY,
390 Styles.eventIcon.fixedWidth,
391 Styles.eventIcon.fixedHeight
392 ).PixelPerfect();
393
394 x += iconPosition.width;
395
396 var clipWidth = innerWidth - (x - innerX);
397
398 clipPosition = new Rect
399 (
400 x,
401 edgeY,
402 clipWidth,
403 edgeHeight
404 ).PixelPerfect();
405
406 labelInnerPosition = new Rect
407 (
408 Styles.spaceAroundIcon,
409 innerY - edgeY,
410 labelWidth,
411 labelHeight
412 ).PixelPerfect();
413 }
414
415 #endregion
416
417
418 #region Drawing
419
420 protected virtual NodeColorMix baseColor => NodeColor.Gray;
421
422 protected override NodeColorMix color
423 {
424 get
425 {
426 if (transitionDebugData.runtimeException != null)
427 {
428 return NodeColor.Red;
429 }
430
431 var color = baseColor;
432
433 if (analysis.warnings.Count > 0)
434 {
435 var mostSevereWarning = Warning.MostSevereLevel(analysis.warnings);
436
437 switch (mostSevereWarning)
438 {
439 case WarningLevel.Error:
440 color = NodeColor.Red;
441
442 break;
443
444 case WarningLevel.Severe:
445 color = NodeColor.Orange;
446
447 break;
448
449 case WarningLevel.Caution:
450 color = NodeColor.Yellow;
451
452 break;
453 }
454 }
455
456 if (EditorApplication.isPlaying)
457 {
458 if (EditorApplication.isPaused)
459 {
460 if (EditorTimeBinding.frame == transitionDebugData.lastBranchFrame)
461 {
462 color = NodeColor.Blue;
463 }
464 }
465 else
466 {
467 color.blue = Mathf.Lerp(1, 0, (EditorTimeBinding.time - transitionDebugData.lastBranchTime) / StateWidget<IState>.Styles.enterFadeDuration);
468 }
469 }
470
471 return color;
472 }
473 }
474
475 protected override NodeShape shape => NodeShape.Hex;
476
477 private bool revealLabel
478 {
479 get
480 {
481 switch (BoltState.Configuration.transitionsReveal)
482 {
483 case StateRevealCondition.Always:
484
485 return true;
486 case StateRevealCondition.Never:
487
488 return false;
489 case StateRevealCondition.OnHover:
490
491 return isMouseOver;
492 case StateRevealCondition.OnHoverWithAlt:
493
494 return isMouseOver && e.alt;
495 case StateRevealCondition.WhenSelected:
496
497 return selection.Contains(transition);
498 case StateRevealCondition.OnHoverOrSelected:
499
500 return isMouseOver || selection.Contains(transition);
501 case StateRevealCondition.OnHoverWithAltOrSelected:
502
503 return isMouseOver && e.alt || selection.Contains(transition);
504 default:
505
506 throw new UnexpectedEnumValueException<StateRevealCondition>(BoltState.Configuration.transitionsReveal);
507 }
508 }
509 }
510
511 private bool revealedLabel;
512
513 private void CheckReveal()
514 {
515 var revealLabel = this.revealLabel;
516
517 if (revealLabel != revealedLabel)
518 {
519 Reposition();
520 }
521
522 revealedLabel = revealLabel;
523 }
524
525 protected override bool dim
526 {
527 get
528 {
529 var dim = BoltCore.Configuration.dimInactiveNodes && !(transition.source.Analysis<StateAnalysis>(context).isEntered && analysis.isTraversed);
530
531 if (isMouseOver || isSelected)
532 {
533 dim = false;
534 }
535
536 return dim;
537 }
538 }
539
540 public override void DrawForeground()
541 {
542 BeginDim();
543
544 base.DrawForeground();
545
546 GUI.Label(iconPosition, label, Styles.eventIcon);
547
548 GUI.BeginClip(clipPosition);
549
550 GUI.Label(labelInnerPosition, label, invertForeground ? Styles.labelInverted : Styles.label);
551
552 GUI.EndClip();
553
554 EndDim();
555
556 CheckReveal();
557 }
558
559 public override void DrawBackground()
560 {
561 BeginDim();
562
563 base.DrawBackground();
564
565 DrawConnection();
566
567 if (showDroplets)
568 {
569 DrawDroplets();
570 }
571
572 EndDim();
573 }
574
575 private void DrawConnection()
576 {
577 GraphGUI.DrawConnectionArrow(Color.white, sourceEdgeCenter, entryEdgeCenter, sourceEdge, entryEdge, relativeBend, minBend);
578
579 if (BoltState.Configuration.transitionsEndArrow)
580 {
581 GraphGUI.DrawConnectionArrow(Color.white, exitEdgeCenter, destinationEdgeCenter, exitEdge, destinationEdge, relativeBend, minBend);
582 }
583 else
584 {
585 GraphGUI.DrawConnection(Color.white, exitEdgeCenter, destinationEdgeCenter, exitEdge, destinationEdge, null, Vector2.zero, relativeBend, minBend);
586 }
587 }
588
589 #endregion
590
591
592 #region Selecting
593
594 public override bool canSelect => true;
595
596 #endregion
597
598
599 #region Dragging
600
601 public override bool canDrag => false;
602
603 protected override bool snapToGrid => false;
604
605 #endregion
606
607
608 #region Deleting
609
610 public override bool canDelete => true;
611
612 #endregion
613
614
615 #region Droplets
616
617 private readonly List<float> droplets = new List<float>();
618
619 private float dropTime;
620
621 private float lastBranchTime;
622
623 protected virtual bool showDroplets => BoltState.Configuration.animateTransitions;
624
625 protected virtual Vector2 GetDropletSize()
626 {
627 return BoltFlow.Icons.valuePortConnected?[12].Size() ?? 12 * Vector2.one;
628 }
629
630 protected virtual void DrawDroplet(Rect position)
631 {
632 GUI.DrawTexture(position, BoltFlow.Icons.valuePortConnected?[12]);
633 }
634
635 private void DrawDroplets()
636 {
637 foreach (var droplet in droplets)
638 {
639 Vector2 position;
640
641 if (droplet < 0.5f)
642 {
643 var t = droplet / 0.5f;
644 position = GraphGUI.GetPointOnConnection(t, sourceEdgeCenter, entryEdgeCenter, sourceEdge, entryEdge, relativeBend, minBend);
645 }
646 else
647 {
648 var t = (droplet - 0.5f) / 0.5f;
649 position = GraphGUI.GetPointOnConnection(t, exitEdgeCenter, destinationEdgeCenter, exitEdge, destinationEdge, relativeBend, minBend);
650 }
651
652 var size = GetDropletSize();
653
654 using (LudiqGUI.color.Override(Color.white))
655 {
656 DrawDroplet(new Rect(position.x - size.x / 2, position.y - size.y / 2, size.x, size.y));
657 }
658 }
659 }
660
661 #endregion
662
663
664 public static class Styles
665 {
666 static Styles()
667 {
668 label = new GUIStyle(BoltCore.Styles.nodeLabel);
669 label.alignment = TextAnchor.MiddleCenter;
670 label.imagePosition = ImagePosition.TextOnly;
671
672 labelInverted = new GUIStyle(label);
673 labelInverted.normal.textColor = ColorPalette.unityBackgroundDark;
674
675 eventIcon = new GUIStyle();
676 eventIcon.imagePosition = ImagePosition.ImageOnly;
677 eventIcon.fixedHeight = 16;
678 eventIcon.fixedWidth = 16;
679 }
680
681 public static readonly GUIStyle label;
682
683 public static readonly GUIStyle labelInverted;
684
685 public static readonly GUIStyle eventIcon;
686
687 public static readonly float spaceAroundIcon = 5;
688
689 public static readonly float revealSpeed = 15;
690 }
691 }
692}