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}