├── .github ├── README.md └── example.png ├── .gitignore ├── Elements ├── Graph │ ├── BaseEdge.cs │ ├── BaseNode.cs │ ├── BasePort.cs │ ├── Edge.cs │ └── GraphElement.cs ├── GraphElementContainer.cs ├── GraphView.cs ├── GridBackground.cs └── Marquee.cs ├── Enums ├── Capabilities.cs ├── Direction.cs ├── Orientation.cs └── PortCapacity.cs ├── GraphViewPlayer.asmdef ├── Interfaces ├── IPositionable.cs ├── ISelectable.cs └── ISelector.cs ├── Manipulators ├── DragAndDropManipulator.cs ├── SelectableManipulator.cs └── ZoomManipulator.cs ├── Resources └── GraphViewPlayer │ └── GraphView.uss └── Utils ├── MouseUtils.cs ├── PlatformUtils.cs └── RectUtils.cs /.github/README.md: -------------------------------------------------------------------------------- 1 | # Graph View Player 2 | ![screenshot](example.png) 3 | 4 | Graph View Player is a _deeply_ refactored version of GraphViewEditor from Unity. Most features have been removed. All that remains are Nodes, Edges, and Ports. No stackable nodes, no collapsible titles, no selection undo/redo, etc. 5 | 6 | The two main goals of this project were to make a node editor that: 7 | - Would run in the Unity player 8 | - Would allow for an asynchronous storage backend rather than default to using ScriptableObjects 9 | 10 | So far so good! The code should be functional and I'll be improving here and there as I make use of the code for a cross platform dialogue system I'm building. 11 | 12 | What follows is an example subclass of GraphView that will allow you to play arround. Just be sure when you add the GraphView to a UI document, you make sure to give it width/height or flex-grow so that it's visible: 13 | 14 | ``` 15 | public class Testing : GraphView 16 | { 17 | protected override void ExecuteCopy() { Debug.Log("OnCopy"); } 18 | 19 | protected override void ExecuteCut() { Debug.Log("OnCut"); } 20 | 21 | protected override void ExecutePaste() { Debug.Log("OnPaste"); } 22 | 23 | protected override void ExecuteDuplicate() { Debug.Log("OnDuplicate"); } 24 | 25 | protected override void ExecuteDelete() { Debug.Log("OnDelete"); } 26 | 27 | protected override void ExecuteUndo() { Debug.Log("OnUndo"); } 28 | 29 | protected override void ExecuteRedo() { Debug.Log("OnRedo"); } 30 | 31 | protected override void ExecuteEdgeCreate(BaseEdge edge) 32 | { 33 | Debug.Log("Edge created"); 34 | AddElement(edge); 35 | } 36 | 37 | protected override void ExecuteEdgeDelete(BaseEdge edge) 38 | { 39 | Debug.Log("Edge deleted"); 40 | RemoveElement(edge); 41 | } 42 | 43 | protected override void OnNodeMoved(BaseNode element) { } 44 | 45 | protected override void OnViewportChanged() { } 46 | 47 | public new class UxmlFactory : UxmlFactory 48 | { 49 | } 50 | } 51 | ``` -------------------------------------------------------------------------------- /.github/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sahasrara/GraphViewPlayer/c25b16da9df077b0c2a3bd8fe629f7f1bdf3d170/.github/example.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Skip .meta and .DS_Store 2 | *.meta 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Elements/Graph/BaseEdge.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using UnityEngine; 6 | using UnityEngine.UIElements; 7 | 8 | namespace GraphViewPlayer 9 | { 10 | public abstract class BaseEdge : GraphElement 11 | { 12 | protected BasePort m_InputPort; 13 | private Vector2 m_InputPortPosition; 14 | protected bool m_InputPositionOverridden; 15 | protected Vector2 m_InputPositionOverride; 16 | 17 | protected BasePort m_OutputPort; 18 | private Vector2 m_OutputPortPosition; 19 | protected bool m_OutputPositionOverridden; 20 | protected Vector2 m_OutputPositionOverride; 21 | 22 | #region Constructor 23 | public BaseEdge() 24 | { 25 | Capabilities 26 | |= Capabilities.Selectable 27 | | Capabilities.Deletable 28 | | Capabilities.Movable; 29 | } 30 | #endregion 31 | 32 | #region Properties 33 | public Vector2 From 34 | { 35 | get 36 | { 37 | if (m_OutputPositionOverridden) { return m_OutputPositionOverride; } 38 | if (Output != null && Graph != null) { return m_OutputPortPosition; } 39 | return Vector2.zero; 40 | } 41 | } 42 | 43 | public Vector2 To 44 | { 45 | get 46 | { 47 | if (m_InputPositionOverridden) { return m_InputPositionOverride; } 48 | if (Input != null && Graph != null) { return m_InputPortPosition; } 49 | return Vector2.zero; 50 | } 51 | } 52 | 53 | public Orientation InputOrientation => Input?.Orientation ?? (Output?.Orientation ?? Orientation.Horizontal); 54 | public Orientation OutputOrientation => Output?.Orientation ?? (Input?.Orientation ?? Orientation.Horizontal); 55 | 56 | public BasePort Output 57 | { 58 | get => m_OutputPort; 59 | set => SetPort(ref m_OutputPort, value); 60 | } 61 | 62 | public BasePort Input 63 | { 64 | get => m_InputPort; 65 | set => SetPort(ref m_InputPort, value); 66 | } 67 | 68 | private void SetPort(ref BasePort portToSet, BasePort newPort) 69 | { 70 | if (newPort != portToSet) 71 | { 72 | // Clean Up Old Connection 73 | if (portToSet != null) 74 | { 75 | UntrackPort(portToSet); 76 | portToSet.Disconnect(this); 77 | } 78 | 79 | // Setup New Connection 80 | portToSet = newPort; 81 | if (portToSet != null) 82 | { 83 | TrackPort(portToSet); 84 | portToSet.Connect(this); 85 | } 86 | 87 | // Mark Dirty 88 | OnEdgeChanged(); 89 | } 90 | } 91 | #endregion 92 | 93 | #region Drag Status 94 | public bool IsRealEdge() => Input != null && Output != null; 95 | public bool IsCandidateEdge() => m_InputPositionOverridden || m_OutputPositionOverridden; 96 | #endregion 97 | 98 | #region Event Handlers 99 | protected virtual void OnGeometryChanged(GeometryChangedEvent e) 100 | { 101 | UpdateCachedInputPortPosition(); 102 | UpdateCachedOutputPortPosition(); 103 | OnEdgeChanged(); 104 | } 105 | 106 | protected override void OnAddedToGraphView() 107 | { 108 | base.OnAddedToGraphView(); 109 | UpdateCachedInputPortPosition(); 110 | UpdateCachedOutputPortPosition(); 111 | } 112 | 113 | protected override void OnRemovedFromGraphView() 114 | { 115 | base.OnRemovedFromGraphView(); 116 | Disconnect(); 117 | UnsetPositionOverrides(); 118 | } 119 | #endregion 120 | 121 | #region Ports 122 | public void SetPortByDirection(BasePort port) 123 | { 124 | if (port.Direction == Direction.Input) { Input = port; } 125 | else { Output = port; } 126 | } 127 | 128 | public void Disconnect() 129 | { 130 | Input = null; 131 | Output = null; 132 | } 133 | 134 | private void TrackPort(BasePort port) 135 | { 136 | port.OnPositionChange += OnPortPositionChanged; 137 | 138 | VisualElement current = port.hierarchy.parent; 139 | while (current != null) 140 | { 141 | if (current is GraphView.Layer) { break; } 142 | 143 | // if we encounter our node ignore it but continue in the case there are nodes inside nodes 144 | if (current != port.ParentNode) { current.RegisterCallback(OnGeometryChanged); } 145 | 146 | current = current.hierarchy.parent; 147 | } 148 | if (port.ParentNode != null) { port.ParentNode.RegisterCallback(OnGeometryChanged); } 149 | 150 | OnPortPositionChanged(new() { element = port }); 151 | } 152 | 153 | private void UntrackPort(BasePort port) 154 | { 155 | port.OnPositionChange -= OnPortPositionChanged; 156 | 157 | VisualElement current = port.hierarchy.parent; 158 | while (current != null) 159 | { 160 | if (current is GraphView.Layer) { break; } 161 | 162 | // if we encounter our node ignore it but continue in the case there are nodes inside nodes 163 | if (current != port.ParentNode) { port.UnregisterCallback(OnGeometryChanged); } 164 | 165 | current = current.hierarchy.parent; 166 | } 167 | if (port.ParentNode != null) 168 | { 169 | port.ParentNode.UnregisterCallback(OnGeometryChanged); 170 | } 171 | } 172 | 173 | private void OnPortPositionChanged(PositionData changeData) 174 | { 175 | BasePort p = changeData.element as BasePort; 176 | if (p != null && Graph != null) 177 | { 178 | if (p == m_InputPort) { UpdateCachedInputPortPosition(); } 179 | else if (p == m_OutputPort) { UpdateCachedOutputPortPosition(); } 180 | OnEdgeChanged(); 181 | } 182 | } 183 | 184 | private void UpdateCachedInputPortPosition() 185 | { 186 | if (Graph == null || Input == null) { return; } 187 | m_InputPortPosition = Graph.ContentContainer.WorldToLocal(Input.GetGlobalCenter()); 188 | if (!m_InputPositionOverridden) { m_InputPositionOverride = m_InputPortPosition; } 189 | } 190 | 191 | private void UpdateCachedOutputPortPosition() 192 | { 193 | if (Graph == null || Output == null) { return; } 194 | m_OutputPortPosition = Graph.ContentContainer.WorldToLocal(Output.GetGlobalCenter()); 195 | if (!m_OutputPositionOverridden) { m_OutputPositionOverride = m_OutputPortPosition; } 196 | } 197 | #endregion 198 | 199 | #region Position 200 | protected abstract void OnEdgeChanged(); 201 | 202 | public override void SetPosition(Vector2 newPosition) 203 | { 204 | if (IsInputPositionOverriden()) { SetInputPositionOverride(newPosition); } 205 | else if (IsOutputPositionOverriden()) { SetOutputPositionOverride(newPosition); } 206 | // If there are no overrides, ignore this because we aren't dragging yet. 207 | } 208 | 209 | public override Vector2 GetPosition() 210 | { 211 | if (IsInputPositionOverriden()) { return GetInputPositionOverride(); } 212 | if (IsOutputPositionOverriden()) { return GetOutputPositionOverride(); } 213 | return Vector2.zero; 214 | } 215 | 216 | public override void ApplyDeltaToPosition(Vector2 delta) { SetPosition(GetPosition() + delta); } 217 | 218 | public Vector2 GetInputPositionOverride() => m_InputPositionOverride; 219 | public Vector2 GetOutputPositionOverride() => m_OutputPositionOverride; 220 | public bool IsInputPositionOverriden() => m_InputPositionOverridden; 221 | public bool IsOutputPositionOverriden() => m_OutputPositionOverridden; 222 | 223 | public void SetInputPositionOverride(Vector2 position) 224 | => SetPositionOverride( 225 | position, out m_InputPositionOverride, ref m_InputPositionOverridden, ref m_InputPort); 226 | 227 | public void SetOutputPositionOverride(Vector2 position) 228 | => SetPositionOverride( 229 | position, out m_OutputPositionOverride, ref m_OutputPositionOverridden, ref m_OutputPort); 230 | 231 | private void SetPositionOverride(Vector2 position, out Vector2 overrideValueToSet, 232 | ref bool overrideFlagToSet, ref BasePort portToUpdate) 233 | { 234 | // Order here is important 235 | // TODO - position is being used as a flag for "drag mode". Should be refactored. 236 | overrideValueToSet = position; 237 | if (overrideFlagToSet != true) 238 | { 239 | overrideFlagToSet = true; 240 | portToUpdate?.UpdateCapColor(); 241 | } 242 | OnEdgeChanged(); 243 | } 244 | 245 | public void UnsetPositionOverrides() 246 | { 247 | m_InputPositionOverridden = false; 248 | m_OutputPositionOverridden = false; 249 | m_InputPort?.UpdateCapColor(); 250 | m_OutputPort?.UpdateCapColor(); 251 | OnEdgeChanged(); 252 | } 253 | 254 | public override Vector2 GetCenter() 255 | { 256 | if (m_InputPositionOverridden) { return m_InputPositionOverride; } 257 | if (m_OutputPositionOverridden) { return m_OutputPositionOverride; } 258 | return base.GetCenter(); 259 | } 260 | #endregion 261 | 262 | #region Drag Events 263 | [EventInterest(typeof(DragOfferEvent))] 264 | protected override void ExecuteDefaultActionAtTarget(EventBase evt) 265 | { 266 | base.ExecuteDefaultActionAtTarget(evt); 267 | if (evt.eventTypeId == DragOfferEvent.TypeId()) { OnDragOffer((DragOfferEvent)evt); } 268 | } 269 | 270 | private void OnDragOffer(DragOfferEvent e) 271 | { 272 | // Check if this is a edge drag event 273 | if (!IsEdgeDrag(e) || !IsMovable()) { return; } 274 | 275 | // Accept Drag 276 | e.AcceptDrag(this); 277 | 278 | // Set Drag Threshold 279 | e.SetDragThreshold(10); 280 | } 281 | 282 | private bool IsEdgeDrag(DragAndDropEvent e) where T : DragAndDropEvent, new() 283 | { 284 | if ((MouseButton)e.button != MouseButton.LeftMouse) { return false; } 285 | if (!e.modifiers.IsNone()) { return false; } 286 | return true; 287 | } 288 | #endregion 289 | } 290 | } -------------------------------------------------------------------------------- /Elements/Graph/BaseNode.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using UnityEngine; 6 | using UnityEngine.UIElements; 7 | 8 | namespace GraphViewPlayer 9 | { 10 | public class BaseNode : GraphElement 11 | { 12 | #region Constructor 13 | public BaseNode() 14 | { 15 | // Root Container 16 | MainContainer = this; 17 | 18 | // Title Label 19 | TitleLabel = new() { pickingMode = PickingMode.Ignore }; 20 | TitleLabel.AddToClassList("node-title-label"); 21 | 22 | // Title Container 23 | TitleContainer = new() { pickingMode = PickingMode.Ignore }; 24 | TitleContainer.AddToClassList("node-title"); 25 | TitleContainer.Add(TitleLabel); 26 | hierarchy.Add(TitleContainer); 27 | 28 | // Input Container 29 | InputContainer = new() { pickingMode = PickingMode.Ignore }; 30 | InputContainer.AddToClassList("node-io-input"); 31 | 32 | // Output Container 33 | OutputContainer = new() { pickingMode = PickingMode.Ignore }; 34 | OutputContainer.AddToClassList("node-io-output"); 35 | 36 | // Top Container 37 | TopContainer = new() { pickingMode = PickingMode.Ignore }; 38 | TopContainer.AddToClassList("node-io"); 39 | TopContainer.Add(InputContainer); 40 | TopContainer.Add(OutputContainer); 41 | hierarchy.Add(TopContainer); 42 | 43 | // Extension Container 44 | ExtensionContainer = new() { pickingMode = PickingMode.Ignore }; 45 | ExtensionContainer.AddToClassList("node-extension"); 46 | hierarchy.Add(ExtensionContainer); 47 | 48 | // Style 49 | AddToClassList("node"); 50 | 51 | // Capability 52 | Capabilities |= Capabilities.Selectable 53 | | Capabilities.Movable 54 | | Capabilities.Deletable 55 | | Capabilities.Ascendable 56 | ; 57 | usageHints = UsageHints.DynamicTransform; 58 | } 59 | #endregion 60 | 61 | #region Properties 62 | protected Label TitleLabel { get; } 63 | protected VisualElement MainContainer { get; } 64 | protected VisualElement TitleContainer { get; } 65 | protected VisualElement TopContainer { get; } 66 | protected VisualElement InputContainer { get; } 67 | protected VisualElement OutputContainer { get; } 68 | public VisualElement ExtensionContainer { get; } 69 | 70 | public override string Title 71 | { 72 | get => TitleLabel != null ? TitleLabel.text : string.Empty; 73 | set 74 | { 75 | if (TitleLabel != null) { TitleLabel.text = value; } 76 | } 77 | } 78 | 79 | public override bool Selected 80 | { 81 | get => base.Selected; 82 | set 83 | { 84 | if (base.Selected == value) { return; } 85 | base.Selected = value; 86 | if (value) { AddToClassList("node-selected"); } 87 | else { RemoveFromClassList("node-selected"); } 88 | } 89 | } 90 | #endregion 91 | 92 | #region Ports 93 | public virtual void AddPort(BasePort port) 94 | { 95 | port.ParentNode = this; 96 | if (port.Direction == Direction.Input) { InputContainer.Add(port); } 97 | else { OutputContainer.Add(port); } 98 | } 99 | #endregion 100 | 101 | #region Drag Events 102 | [EventInterest(typeof(DragOfferEvent))] 103 | protected override void ExecuteDefaultActionAtTarget(EventBase evt) 104 | { 105 | base.ExecuteDefaultActionAtTarget(evt); 106 | if (evt.eventTypeId == DragOfferEvent.TypeId()) { OnDragOffer((DragOfferEvent)evt); } 107 | } 108 | 109 | private void OnDragOffer(DragOfferEvent e) 110 | { 111 | // Check if this is a node drag event 112 | if (!IsNodeDrag(e) || !IsMovable()) { return; } 113 | 114 | // Accept Drag 115 | e.AcceptDrag(this); 116 | } 117 | 118 | private bool IsNodeDrag(DragAndDropEvent e) where T : DragAndDropEvent, new() 119 | { 120 | if ((MouseButton)e.button != MouseButton.LeftMouse) { return false; } 121 | if (!e.modifiers.IsNone()) { return false; } 122 | return true; 123 | } 124 | #endregion 125 | } 126 | } -------------------------------------------------------------------------------- /Elements/Graph/BasePort.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using UnityEngine; 8 | using UnityEngine.UIElements; 9 | 10 | namespace GraphViewPlayer 11 | { 12 | public class BasePort : VisualElement, IPositionable 13 | { 14 | private static readonly CustomStyleProperty s_PortColorProperty = new("--port-color"); 15 | private static readonly CustomStyleProperty s_DisabledPortColorProperty = new("--disabled-port-color"); 16 | 17 | private static readonly Color s_DefaultColor = new(240 / 255f, 240 / 255f, 240 / 255f); 18 | private static readonly Color s_DefaultDisabledColor = new(70 / 255f, 70 / 255f, 70 / 255f); 19 | 20 | private readonly HashSet m_Connections; 21 | protected VisualElement m_ConnectorBox; 22 | protected VisualElement m_ConnectorBoxCap; 23 | protected Label m_ConnectorText; 24 | private Direction m_Direction; 25 | private bool m_Highlight = true; 26 | private BaseNode m_ParentNode; 27 | private Color m_PortColor = s_DefaultColor; 28 | private bool m_PortColorIsInline; 29 | 30 | #region Constructor 31 | internal BasePort(Orientation orientation, Direction direction, PortCapacity capacity) 32 | { 33 | ClearClassList(); 34 | 35 | // Label 36 | m_ConnectorText = new() { pickingMode = PickingMode.Ignore }; 37 | m_ConnectorText.AddToClassList("port-label"); 38 | Add(m_ConnectorText); 39 | 40 | // Cap 41 | m_ConnectorBoxCap = new() { pickingMode = PickingMode.Ignore }; 42 | m_ConnectorBoxCap.AddToClassList("port-connector-cap"); 43 | 44 | // Box 45 | m_ConnectorBox = new() { pickingMode = PickingMode.Ignore }; 46 | m_ConnectorBox.AddToClassList("port-connector-box"); 47 | m_ConnectorBox.Add(m_ConnectorBoxCap); 48 | Add(m_ConnectorBox); 49 | 50 | m_Connections = new(); 51 | 52 | Orientation = orientation; 53 | Direction = direction; 54 | Capacity = capacity; 55 | 56 | AddToClassList("port"); 57 | RegisterCallback(OnCustomStyleResolved); 58 | } 59 | #endregion 60 | 61 | #region Properties 62 | public BaseNode ParentNode 63 | { 64 | get => m_ParentNode; 65 | internal set 66 | { 67 | if (m_ParentNode == value) { return; } 68 | if (m_ParentNode != null) { m_ParentNode.OnPositionChange -= OnParentPositionChange; } 69 | if (value == null) { m_ParentNode = null; } 70 | else 71 | { 72 | m_ParentNode = value; 73 | m_ParentNode.OnPositionChange += OnParentPositionChange; 74 | } 75 | } 76 | } 77 | 78 | internal Color CapColor 79 | { 80 | get 81 | { 82 | if (m_ConnectorBoxCap == null) { return Color.black; } 83 | return m_ConnectorBoxCap.resolvedStyle.backgroundColor; 84 | } 85 | 86 | set 87 | { 88 | if (m_ConnectorBoxCap != null) { m_ConnectorBoxCap.style.backgroundColor = value; } 89 | } 90 | } 91 | 92 | public string PortName 93 | { 94 | get => m_ConnectorText.text; 95 | set => m_ConnectorText.text = value; 96 | } 97 | 98 | public Direction Direction 99 | { 100 | get => m_Direction; 101 | private set 102 | { 103 | RemoveFromClassList($"port-{m_Direction.ToString().ToLower()}"); 104 | m_Direction = value; 105 | AddToClassList($"port-{m_Direction.ToString().ToLower()}"); 106 | } 107 | } 108 | 109 | public bool Highlight 110 | { 111 | get => m_Highlight; 112 | set 113 | { 114 | if (m_Highlight == value) { return; } 115 | 116 | m_Highlight = value; 117 | 118 | UpdateConnectorColorAndEnabledState(); 119 | } 120 | } 121 | 122 | public Color PortColor 123 | { 124 | get => m_PortColor; 125 | set 126 | { 127 | m_PortColorIsInline = true; 128 | m_PortColor = value; 129 | UpdateCapColor(); 130 | } 131 | } 132 | 133 | public Color DisabledPortColor { get; private set; } = s_DefaultDisabledColor; 134 | public Orientation Orientation { get; } 135 | public PortCapacity Capacity { get; } 136 | public bool AllowMultiDrag { get; set; } = true; 137 | public virtual IEnumerable Connections => m_Connections; 138 | #endregion 139 | 140 | #region Edges 141 | public virtual bool Connected(bool ignoreCandidateEdges = true) 142 | { 143 | foreach (BaseEdge edge in m_Connections) 144 | { 145 | if (ignoreCandidateEdges && edge.IsCandidateEdge()) { continue; } 146 | return true; 147 | } 148 | return false; 149 | } 150 | 151 | public BaseEdge ConnectTo(BasePort other) 152 | { 153 | if (other == null) { throw new ArgumentNullException(nameof(other)); } 154 | 155 | if (other.Direction == Direction) 156 | { 157 | throw new ArgumentException("Cannot connect two ports with the same direction"); 158 | } 159 | BaseEdge edge = ParentNode.Graph.CreateEdge(); 160 | edge.Output = Direction == Direction.Output ? this : other; 161 | edge.Input = Direction == Direction.Input ? this : other; 162 | return edge; 163 | } 164 | 165 | public virtual void Connect(BaseEdge edge) 166 | { 167 | if (edge == null) { throw new ArgumentException("The value passed to Port.Connect is null"); } 168 | 169 | if (!m_Connections.Contains(edge)) { m_Connections.Add(edge); } 170 | 171 | UpdateCapColor(); 172 | } 173 | 174 | public virtual void Disconnect(BaseEdge edge) 175 | { 176 | if (edge == null) { throw new ArgumentException("The value passed to PortPresenter.Disconnect is null"); } 177 | 178 | m_Connections.Remove(edge); 179 | UpdateCapColor(); 180 | } 181 | 182 | public virtual bool CanConnectToMore(bool ignoreCandidateEdges = true) 183 | => Capacity == PortCapacity.Multi || !Connected(ignoreCandidateEdges); 184 | 185 | public bool IsConnectedTo(BasePort other, bool ignoreCandidateEdges = true) 186 | { 187 | foreach (BaseEdge e in m_Connections) 188 | { 189 | if (ignoreCandidateEdges && e.IsCandidateEdge()) { continue; } 190 | if (Direction == Direction.Output) 191 | { 192 | if (e.Input == other) { return true; } 193 | } 194 | else 195 | { 196 | if (e.Output == other) { return true; } 197 | } 198 | } 199 | return false; 200 | } 201 | 202 | public bool SameDirection(BasePort other) => Direction == other.Direction; 203 | 204 | public bool IsOnSameNode(BasePort other) => other.ParentNode == ParentNode; 205 | 206 | public virtual bool CanConnectTo(BasePort other, bool ignoreCandidateEdges = true) 207 | { 208 | // Debug.Log($"{this} - {other} >> same_direction: {SameDirection(other)}," 209 | // + $"same_node: {IsOnSameNode(other)}, " 210 | // + $"has_cap: {CanConnectToMore(ignoreCandidateEdges)}, " 211 | // + $"other_has_cap: {other.CanConnectToMore(ignoreCandidateEdges)}, " 212 | // + $"is_connected: {IsConnectedTo(other, ignoreCandidateEdges)}"); 213 | return !SameDirection(other) 214 | && !IsOnSameNode(other) 215 | && CanConnectToMore(ignoreCandidateEdges) 216 | && other.CanConnectToMore(ignoreCandidateEdges) 217 | && !IsConnectedTo(other, ignoreCandidateEdges); 218 | } 219 | #endregion 220 | 221 | #region Style 222 | internal void UpdateCapColor() 223 | { 224 | if (Connected()) { m_ConnectorBoxCap.style.backgroundColor = PortColor; } 225 | else { m_ConnectorBoxCap.style.backgroundColor = StyleKeyword.Null; } 226 | } 227 | 228 | private void UpdateConnectorColorAndEnabledState() 229 | { 230 | if (m_ConnectorBox == null) { return; } 231 | 232 | Color color = Highlight ? m_PortColor : DisabledPortColor; 233 | m_ConnectorBox.style.borderLeftColor = color; 234 | m_ConnectorBox.style.borderTopColor = color; 235 | m_ConnectorBox.style.borderRightColor = color; 236 | m_ConnectorBox.style.borderBottomColor = color; 237 | m_ConnectorBox.SetEnabled(Highlight); 238 | } 239 | 240 | private void OnCustomStyleResolved(CustomStyleResolvedEvent evt) 241 | { 242 | if (!m_PortColorIsInline && evt.customStyle.TryGetValue(s_PortColorProperty, out Color portColorValue)) 243 | { 244 | m_PortColor = portColorValue; 245 | UpdateCapColor(); 246 | } 247 | if (evt.customStyle.TryGetValue(s_DisabledPortColorProperty, out Color disableColorValue)) 248 | { 249 | DisabledPortColor = disableColorValue; 250 | } 251 | UpdateConnectorColorAndEnabledState(); 252 | } 253 | #endregion 254 | 255 | #region Position 256 | public event Action OnPositionChange; 257 | public Vector2 GetGlobalCenter() => m_ConnectorBox.LocalToWorld(GetCenter()); 258 | public Vector2 GetCenter() => new Rect(Vector2.zero, m_ConnectorBox.layout.size).center; 259 | public Vector2 GetPosition() => Vector2.zero; 260 | public void SetPosition(Vector2 position) => throw new NotImplementedException(); 261 | public void ApplyDeltaToPosition(Vector2 delta) => throw new NotImplementedException(); 262 | private void OnParentPositionChange(PositionData positionData) 263 | => OnPositionChange?.Invoke(new() { element = this }); 264 | #endregion 265 | 266 | #region Event Handlers 267 | [EventInterest(typeof(DragOfferEvent), typeof(DropEnterEvent), typeof(DropEvent), typeof(DropExitEvent))] 268 | protected override void ExecuteDefaultActionAtTarget(EventBase evt) 269 | { 270 | base.ExecuteDefaultActionAtTarget(evt); 271 | if (evt.eventTypeId == DragOfferEvent.TypeId()) OnDragOffer((DragOfferEvent)evt); 272 | else if (evt.eventTypeId == DropEnterEvent.TypeId()) OnDropEnter((DropEnterEvent)evt); 273 | else if (evt.eventTypeId == DropEvent.TypeId()) OnDrop((DropEvent)evt); 274 | else if (evt.eventTypeId == DropExitEvent.TypeId()) OnDropExit((DropExitEvent)evt); 275 | } 276 | 277 | private void OnDragOffer(DragOfferEvent e) 278 | { 279 | // Check if this is a port drag event 280 | if (!IsPortDrag(e) || !CanConnectToMore()) return; 281 | 282 | // Create edge 283 | BaseEdge draggedEdge = ParentNode.Graph.CreateEdge(); 284 | draggedEdge.SetPortByDirection(this); 285 | draggedEdge.visible = false; 286 | ParentNode.Graph.AddElement(draggedEdge); 287 | 288 | // Accept drag 289 | e.AcceptDrag(draggedEdge); 290 | 291 | // Set threshold 292 | e.SetDragThreshold(10); 293 | } 294 | 295 | private void OnDropEnter(DropEnterEvent e) 296 | { 297 | if (e.GetUserData() is IDropPayload dropPayload && typeof(BaseEdge).IsAssignableFrom(dropPayload.GetPayloadType())) 298 | { 299 | // Consume event 300 | e.StopImmediatePropagation(); 301 | 302 | // Sanity 303 | if (dropPayload.GetPayload().Count == 0) throw new("Drop payload was unexpectedly empty"); 304 | 305 | // Grab dragged port 306 | BaseEdge draggedEdge = (BaseEdge) dropPayload.GetPayload()[0]; 307 | BasePort anchoredPort = draggedEdge.IsInputPositionOverriden() ? draggedEdge.Output : draggedEdge.Input; 308 | 309 | // Ignore drags from invalid ports 310 | if (!CanConnectTo(anchoredPort)) 311 | { 312 | return; 313 | } 314 | 315 | // But if it's compatible, light this port up 316 | m_ConnectorBoxCap.style.backgroundColor = PortColor; 317 | } 318 | } 319 | 320 | private void OnDrop(DropEvent e) 321 | { 322 | if (e.GetUserData() is IDropPayload dropPayload && typeof(BaseEdge).IsAssignableFrom(dropPayload.GetPayloadType())) 323 | { 324 | // Consume event 325 | e.StopImmediatePropagation(); 326 | 327 | // Sanity 328 | if (dropPayload.GetPayload().Count == 0) throw new("Drop payload was unexpectedly empty"); 329 | 330 | // Grab dragged port (iterate backwards since this can result in deletions) 331 | for (int i = dropPayload.GetPayload().Count - 1; i >= 0; i--) 332 | { 333 | // Grab dragged edge and the corresponding anchored port 334 | BaseEdge edge = (BaseEdge) dropPayload.GetPayload()[i]; 335 | BasePort anchoredPort = edge.IsInputPositionOverriden() ? edge.Output : edge.Input; 336 | ConnectToPortWithEdge(e, anchoredPort, edge); 337 | } 338 | 339 | // But if it's compatible, update caps as appropriate 340 | UpdateCapColor(); 341 | } 342 | } 343 | 344 | private void OnDropExit(DropExitEvent e) 345 | { 346 | if (e.GetUserData() is IDropPayload dropPayload && typeof(BaseEdge).IsAssignableFrom(dropPayload.GetPayloadType())) 347 | { 348 | // Consume event 349 | e.StopImmediatePropagation(); 350 | 351 | // Sanity 352 | if (dropPayload.GetPayload().Count == 0) throw new("Drop payload was unexpectedly empty"); 353 | 354 | // But if it's compatible, update caps as appropriate 355 | UpdateCapColor(); 356 | } 357 | } 358 | 359 | private bool IsPortDrag(DragAndDropEvent e) where T : DragAndDropEvent, new() 360 | { 361 | if ((MouseButton)e.button != MouseButton.LeftMouse) { return false; } 362 | if (!e.modifiers.IsNone()) { return false; } 363 | return true; 364 | } 365 | 366 | private void ConnectToPortWithEdge(DropEvent e, BasePort anchoredPort, BaseEdge draggedEdge) 367 | { 368 | // Cancel invalid drags or already extant edges 369 | if (!CanConnectTo(anchoredPort)) 370 | { 371 | // Real edges will be returned to their former state 372 | e.CancelDrag(); 373 | return; 374 | } 375 | 376 | // Capture the ports being connected 377 | BasePort inputPort; 378 | BasePort outputPort; 379 | if (anchoredPort.Direction == Direction.Input) 380 | { 381 | inputPort = anchoredPort; 382 | outputPort = this; 383 | } 384 | else 385 | { 386 | inputPort = this; 387 | outputPort = anchoredPort; 388 | } 389 | 390 | // If this is an existing edge, delete it unless we're reconnecting after a disconnect 391 | if (draggedEdge.IsRealEdge()) 392 | { 393 | if (draggedEdge.Input == inputPort && draggedEdge.Output == outputPort) 394 | { 395 | e.CancelDrag(); 396 | return; 397 | } 398 | ParentNode.Graph.ExecuteEdgeDelete(draggedEdge); 399 | } 400 | 401 | // This was a temporary edge, reset it and remove from the graph 402 | else { ParentNode.Graph.RemoveElement(draggedEdge); } 403 | 404 | // Create new edge (reusing the old deleted edge) 405 | draggedEdge.Input = inputPort; 406 | draggedEdge.Output = outputPort; 407 | ParentNode.Graph.ExecuteEdgeCreate(draggedEdge); 408 | } 409 | #endregion 410 | 411 | public override string ToString() => PortName; 412 | } 413 | } -------------------------------------------------------------------------------- /Elements/Graph/Edge.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using UnityEngine; 8 | using UnityEngine.Profiling; 9 | using UnityEngine.UIElements; 10 | 11 | namespace GraphViewPlayer 12 | { 13 | public class Edge : BaseEdge 14 | { 15 | public const float k_MinEdgeWidth = 1.75f; 16 | private const float k_EndPointRadius = 4.0f; 17 | private const float k_InterceptWidth = 6.0f; 18 | private const float k_EdgeLengthFromPort = 12.0f; 19 | private const float k_EdgeTurnDiameter = 16.0f; 20 | private const float k_EdgeSweepResampleRatio = 4.0f; 21 | private const int k_EdgeStraightLineSegmentDivisor = 5; 22 | private const int k_DefaultEdgeWidth = 2; 23 | private const int k_DefaultEdgeWidthSelected = 2; 24 | 25 | private static readonly CustomStyleProperty s_EdgeWidthProperty = new("--edge-width"); 26 | private static readonly CustomStyleProperty s_EdgeWidthSelectedProperty = new("--edge-width-selected"); 27 | private static readonly CustomStyleProperty s_EdgeColorSelectedProperty = new("--edge-color-selected"); 28 | private static readonly CustomStyleProperty s_EdgeColorProperty = new("--edge-color"); 29 | private static readonly Color s_DefaultSelectedColor = new(240 / 255f, 240 / 255f, 240 / 255f); 30 | private static readonly Color s_DefaultColor = new(146 / 255f, 146 / 255f, 146 / 255f); 31 | 32 | private static readonly Gradient s_Gradient = new(); 33 | private static readonly Stack s_CapPool = new(); 34 | private readonly List m_LastLocalControlPoints = new(); 35 | 36 | // The points that will be rendered. Expressed in coordinates local to the element. 37 | private readonly List m_RenderPoints = new(); 38 | private float m_CapRadius = 5; 39 | private bool m_ControlPointsDirty = true; 40 | 41 | private int m_EdgeWidth = 2; 42 | private VisualElement m_FromCap; 43 | private Color m_FromCapColor; 44 | private Color m_InputColor = Color.grey; 45 | private Orientation m_InputOrientation; 46 | private Color m_OutputColor = Color.grey; 47 | private Orientation m_OutputOrientation; 48 | private bool m_RenderPointsDirty = true; 49 | private VisualElement m_ToCap; 50 | private Color m_ToCapColor; 51 | 52 | #region Static Helpers 53 | private static bool Approximately(Vector2 v1, Vector2 v2) => 54 | Mathf.Approximately(v1.x, v2.x) && Mathf.Approximately(v1.y, v2.y); 55 | 56 | private static void RecycleCap(VisualElement cap) { s_CapPool.Push(cap); } 57 | 58 | private static VisualElement GetCap() 59 | { 60 | VisualElement result; 61 | if (s_CapPool.Count > 0) { result = s_CapPool.Pop(); } 62 | else 63 | { 64 | result = new(); 65 | result.AddToClassList("edge-cap"); 66 | } 67 | 68 | return result; 69 | } 70 | #endregion 71 | 72 | #region Constructor 73 | public Edge() 74 | { 75 | ClearClassList(); 76 | AddToClassList("edge"); 77 | m_FromCap = null; 78 | m_ToCap = null; 79 | CapRadius = k_EndPointRadius; 80 | InterceptWidth = k_InterceptWidth; 81 | generateVisualContent = OnGenerateVisualContent; 82 | } 83 | #endregion 84 | 85 | #region Properties 86 | public override bool Selected 87 | { 88 | get => base.Selected; 89 | set 90 | { 91 | if (base.Selected == value) { return; } 92 | base.Selected = value; 93 | if (value) 94 | { 95 | InputColor = ColorSelected; 96 | OutputColor = ColorSelected; 97 | EdgeWidth = EdgeWidthSelected; 98 | } 99 | else 100 | { 101 | 102 | InputColor = ColorUnselected; 103 | OutputColor = ColorUnselected; 104 | EdgeWidth = EdgeWidthUnselected; 105 | } 106 | } 107 | } 108 | 109 | public Color InputColor 110 | { 111 | get => m_InputColor; 112 | set 113 | { 114 | if (m_InputColor != value) 115 | { 116 | m_InputColor = value; 117 | MarkDirtyRepaint(); 118 | } 119 | } 120 | } 121 | 122 | public Color OutputColor 123 | { 124 | get => m_OutputColor; 125 | set 126 | { 127 | if (m_OutputColor != value) 128 | { 129 | m_OutputColor = value; 130 | MarkDirtyRepaint(); 131 | } 132 | } 133 | } 134 | 135 | public Color FromCapColor 136 | { 137 | get => m_FromCapColor; 138 | set 139 | { 140 | if (m_FromCapColor == value) { return; } 141 | m_FromCapColor = value; 142 | 143 | if (m_FromCap != null) { m_FromCap.style.backgroundColor = m_FromCapColor; } 144 | MarkDirtyRepaint(); 145 | } 146 | } 147 | 148 | public Color ToCapColor 149 | { 150 | get => m_ToCapColor; 151 | set 152 | { 153 | if (m_ToCapColor == value) { return; } 154 | m_ToCapColor = value; 155 | 156 | if (m_ToCap != null) { m_ToCap.style.backgroundColor = m_ToCapColor; } 157 | MarkDirtyRepaint(); 158 | } 159 | } 160 | 161 | public float CapRadius 162 | { 163 | get => m_CapRadius; 164 | set 165 | { 166 | if (Mathf.Approximately(m_CapRadius, value)) { return; } 167 | m_CapRadius = value; 168 | MarkDirtyRepaint(); 169 | } 170 | } 171 | 172 | public int EdgeWidth 173 | { 174 | get => m_EdgeWidth; 175 | set 176 | { 177 | if (m_EdgeWidth == value) { return; } 178 | m_EdgeWidth = value; 179 | UpdateLayout(); // The layout depends on the edges width 180 | } 181 | } 182 | 183 | public bool DrawFromCap 184 | { 185 | get => m_FromCap != null; 186 | set 187 | { 188 | if (!value) 189 | { 190 | if (m_FromCap != null) 191 | { 192 | m_FromCap.RemoveFromHierarchy(); 193 | RecycleCap(m_FromCap); 194 | m_FromCap = null; 195 | } 196 | } 197 | else 198 | { 199 | if (m_FromCap == null) 200 | { 201 | m_FromCap = GetCap(); 202 | m_FromCap.style.backgroundColor = m_FromCapColor; 203 | Add(m_FromCap); 204 | } 205 | } 206 | } 207 | } 208 | 209 | public bool DrawToCap 210 | { 211 | get => m_ToCap != null; 212 | set 213 | { 214 | if (!value) 215 | { 216 | if (m_ToCap != null) 217 | { 218 | m_ToCap.RemoveFromHierarchy(); 219 | RecycleCap(m_ToCap); 220 | m_ToCap = null; 221 | } 222 | } 223 | else 224 | { 225 | if (m_ToCap == null) 226 | { 227 | m_ToCap = GetCap(); 228 | m_ToCap.style.backgroundColor = m_ToCapColor; 229 | Add(m_ToCap); 230 | } 231 | } 232 | } 233 | } 234 | 235 | public int EdgeWidthUnselected { get; } = k_DefaultEdgeWidth; 236 | public int EdgeWidthSelected { get; private set; } = k_DefaultEdgeWidthSelected; 237 | public Color ColorSelected { get; private set; } = s_DefaultSelectedColor; 238 | public Color ColorUnselected { get; private set; } = s_DefaultColor; 239 | public float InterceptWidth { get; set; } = 5f; 240 | public Vector2[] ControlPoints { get; private set; } 241 | #endregion 242 | 243 | #region Rendering 244 | private void UpdateEdgeCaps() 245 | { 246 | if (m_FromCap != null) 247 | { 248 | Vector2 size = m_FromCap.layout.size; 249 | if (size.x > 0 && size.y > 0) 250 | { 251 | Rect rect = new(From - size / 2f, size); 252 | m_FromCap.style.left = rect.x; 253 | m_FromCap.style.top = rect.y; 254 | m_FromCap.style.width = rect.width; 255 | m_FromCap.style.height = rect.height; 256 | } 257 | } 258 | if (m_ToCap != null) 259 | { 260 | Vector2 size = m_ToCap.layout.size; 261 | if (size.x > 0 && size.y > 0) 262 | { 263 | Rect rect = new(To - size / 2f, size); 264 | m_ToCap.style.left = rect.x; 265 | m_ToCap.style.top = rect.y; 266 | m_ToCap.style.width = rect.width; 267 | m_ToCap.style.height = rect.height; 268 | } 269 | } 270 | } 271 | 272 | public virtual void UpdateLayout() 273 | { 274 | if (Graph == null) { return; } 275 | if (m_ControlPointsDirty) 276 | { 277 | ComputeControlPoints(); // Computes the control points in parent ( graph ) coordinates 278 | ComputeLayout(); // Update the element layout based on the control points. 279 | m_ControlPointsDirty = false; 280 | } 281 | UpdateEdgeCaps(); 282 | MarkDirtyRepaint(); 283 | } 284 | 285 | private void RenderStraightLines(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4) 286 | { 287 | float safeSpan = OutputOrientation == Orientation.Horizontal 288 | ? Mathf.Abs(p1.x + k_EdgeLengthFromPort - (p4.x - k_EdgeLengthFromPort)) 289 | : Mathf.Abs(p1.y + k_EdgeLengthFromPort - (p4.y - k_EdgeLengthFromPort)); 290 | 291 | float safeSpan3 = safeSpan / k_EdgeStraightLineSegmentDivisor; 292 | float nodeToP2Dist = Mathf.Min(safeSpan3, k_EdgeTurnDiameter); 293 | nodeToP2Dist = Mathf.Max(0, nodeToP2Dist); 294 | 295 | Vector2 offset = OutputOrientation == Orientation.Horizontal 296 | ? new(k_EdgeTurnDiameter - nodeToP2Dist, 0) 297 | : new Vector2(0, k_EdgeTurnDiameter - nodeToP2Dist); 298 | 299 | m_RenderPoints.Add(p1); 300 | m_RenderPoints.Add(p2 - offset); 301 | m_RenderPoints.Add(p3 + offset); 302 | m_RenderPoints.Add(p4); 303 | } 304 | 305 | protected virtual void UpdateRenderPoints() 306 | { 307 | ComputeControlPoints(); // This should have been updated before : make sure anyway. 308 | 309 | if (m_RenderPointsDirty == false && ControlPoints != null) { return; } 310 | 311 | Vector2 p1 = Graph.ContentContainer.ChangeCoordinatesTo(this, ControlPoints[0]); 312 | Vector2 p2 = Graph.ContentContainer.ChangeCoordinatesTo(this, ControlPoints[1]); 313 | Vector2 p3 = Graph.ContentContainer.ChangeCoordinatesTo(this, ControlPoints[2]); 314 | Vector2 p4 = Graph.ContentContainer.ChangeCoordinatesTo(this, ControlPoints[3]); 315 | 316 | // Only compute this when the "local" points have actually changed 317 | if (m_LastLocalControlPoints.Count == 4) 318 | { 319 | if (Approximately(p1, m_LastLocalControlPoints[0]) && 320 | Approximately(p2, m_LastLocalControlPoints[1]) && 321 | Approximately(p3, m_LastLocalControlPoints[2]) && 322 | Approximately(p4, m_LastLocalControlPoints[3])) 323 | { 324 | m_RenderPointsDirty = false; 325 | return; 326 | } 327 | } 328 | 329 | Profiler.BeginSample("EdgeControl.UpdateRenderPoints"); 330 | m_LastLocalControlPoints.Clear(); 331 | m_LastLocalControlPoints.Add(p1); 332 | m_LastLocalControlPoints.Add(p2); 333 | m_LastLocalControlPoints.Add(p3); 334 | m_LastLocalControlPoints.Add(p4); 335 | m_RenderPointsDirty = false; 336 | 337 | m_RenderPoints.Clear(); 338 | 339 | float diameter = k_EdgeTurnDiameter; 340 | 341 | // We have to handle a special case of the edge when it is a straight line, but not 342 | // when going backwards in space (where the start point is in front in y to the end point). 343 | // We do this by turning the line into 3 linear segments with no curves. This also 344 | // avoids possible NANs in later angle calculations. 345 | bool sameOrientations = OutputOrientation == InputOrientation; 346 | if (sameOrientations && 347 | ((OutputOrientation == Orientation.Horizontal && Mathf.Abs(p1.y - p4.y) < 2 && 348 | p1.x + k_EdgeLengthFromPort < p4.x - k_EdgeLengthFromPort) || 349 | (OutputOrientation == Orientation.Vertical && Mathf.Abs(p1.x - p4.x) < 2 && 350 | p1.y + k_EdgeLengthFromPort < p4.y - k_EdgeLengthFromPort))) 351 | { 352 | RenderStraightLines(p1, p2, p3, p4); 353 | Profiler.EndSample(); 354 | return; 355 | } 356 | 357 | bool renderBothCorners = true; 358 | 359 | EdgeCornerSweepValues corner1 = GetCornerSweepValues(p1, p2, p3, diameter, Direction.Output); 360 | EdgeCornerSweepValues corner2 = GetCornerSweepValues(p2, p3, p4, diameter, Direction.Input); 361 | 362 | if (!ValidateCornerSweepValues(ref corner1, ref corner2)) 363 | { 364 | if (sameOrientations) 365 | { 366 | RenderStraightLines(p1, p2, p3, p4); 367 | Profiler.EndSample(); 368 | return; 369 | } 370 | 371 | renderBothCorners = false; 372 | 373 | //we try to do it with a single corner instead 374 | Vector2 px = OutputOrientation == Orientation.Horizontal ? new(p4.x, p1.y) : new Vector2(p1.x, p4.y); 375 | 376 | corner1 = GetCornerSweepValues(p1, px, p4, diameter, Direction.Output); 377 | } 378 | 379 | m_RenderPoints.Add(p1); 380 | 381 | if (!sameOrientations && renderBothCorners) 382 | { 383 | //if the 2 corners or endpoints are too close, the corner sweep angle calculations can't handle different orientations 384 | float minDistance = 2 * diameter * diameter; 385 | if ((p3 - p2).sqrMagnitude < minDistance || 386 | (p4 - p1).sqrMagnitude < minDistance) 387 | { 388 | Vector2 px = (p2 + p3) * 0.5f; 389 | corner1 = GetCornerSweepValues(p1, px, p4, diameter, Direction.Output); 390 | renderBothCorners = false; 391 | } 392 | } 393 | 394 | GetRoundedCornerPoints(m_RenderPoints, corner1, Direction.Output); 395 | if (renderBothCorners) { GetRoundedCornerPoints(m_RenderPoints, corner2, Direction.Input); } 396 | 397 | m_RenderPoints.Add(p4); 398 | Profiler.EndSample(); 399 | } 400 | 401 | private bool ValidateCornerSweepValues(ref EdgeCornerSweepValues corner1, ref EdgeCornerSweepValues corner2) 402 | { 403 | // Get the midpoint between the two corner circle centers. 404 | Vector2 circlesMidpoint = (corner1.circleCenter + corner2.circleCenter) / 2; 405 | 406 | // Find the angle to the corner circles midpoint so we can compare it to the sweep angles of each corner. 407 | Vector2 p2CenterToCross1 = corner1.circleCenter - corner1.crossPoint1; 408 | Vector2 p2CenterToCirclesMid = corner1.circleCenter - circlesMidpoint; 409 | double angleToCirclesMid = OutputOrientation == Orientation.Horizontal 410 | ? Math.Atan2(p2CenterToCross1.y, p2CenterToCross1.x) - 411 | Math.Atan2(p2CenterToCirclesMid.y, p2CenterToCirclesMid.x) 412 | : Math.Atan2(p2CenterToCross1.x, p2CenterToCross1.y) - 413 | Math.Atan2(p2CenterToCirclesMid.x, p2CenterToCirclesMid.y); 414 | 415 | if (double.IsNaN(angleToCirclesMid)) { return false; } 416 | 417 | // We need the angle to the circles midpoint to match the turn direction of the first corner's sweep angle. 418 | angleToCirclesMid = Math.Sign(angleToCirclesMid) * 2 * Mathf.PI - angleToCirclesMid; 419 | if (Mathf.Abs((float)angleToCirclesMid) > 1.5 * Mathf.PI) 420 | { 421 | angleToCirclesMid = -1 * Math.Sign(angleToCirclesMid) * 2 * Mathf.PI + angleToCirclesMid; 422 | } 423 | 424 | // Calculate the maximum sweep angle so that both corner sweeps and with the tangents of the 2 circles meeting each other. 425 | float h = p2CenterToCirclesMid.magnitude; 426 | float p2AngleToMidTangent = Mathf.Acos(corner1.radius / h); 427 | 428 | if (double.IsNaN(p2AngleToMidTangent)) { return false; } 429 | 430 | float maxSweepAngle = Mathf.Abs((float)corner1.sweepAngle) - p2AngleToMidTangent * 2; 431 | 432 | // If the angle to the circles midpoint is within the sweep angle, we need to apply our maximum sweep angle 433 | // calculated above, otherwise the maximum sweep angle is irrelevant. 434 | if (Mathf.Abs((float)angleToCirclesMid) < Mathf.Abs((float)corner1.sweepAngle)) 435 | { 436 | corner1.sweepAngle = Math.Sign(corner1.sweepAngle) * 437 | Mathf.Min(maxSweepAngle, Mathf.Abs((float)corner1.sweepAngle)); 438 | corner2.sweepAngle = Math.Sign(corner2.sweepAngle) * 439 | Mathf.Min(maxSweepAngle, Mathf.Abs((float)corner2.sweepAngle)); 440 | } 441 | 442 | return true; 443 | } 444 | 445 | private EdgeCornerSweepValues GetCornerSweepValues( 446 | Vector2 p1, Vector2 cornerPoint, Vector2 p2, float diameter, Direction closestPortDirection) 447 | { 448 | EdgeCornerSweepValues corner = new(); 449 | 450 | // Calculate initial radius. This radius can change depending on the sharpness of the corner. 451 | corner.radius = diameter / 2; 452 | 453 | // Calculate vectors from p1 to cornerPoint. 454 | Vector2 d1Corner = (cornerPoint - p1).normalized; 455 | Vector2 d1 = d1Corner * diameter; 456 | float dx1 = d1.x; 457 | float dy1 = d1.y; 458 | 459 | // Calculate vectors from p2 to cornerPoint. 460 | Vector2 d2Corner = (cornerPoint - p2).normalized; 461 | Vector2 d2 = d2Corner * diameter; 462 | float dx2 = d2.x; 463 | float dy2 = d2.y; 464 | 465 | // Calculate the angle of the corner (divided by 2). 466 | float angle = (float)(Math.Atan2(dy1, dx1) - Math.Atan2(dy2, dx2)) / 2; 467 | 468 | // Calculate the length of the segment between the cornerPoint and where 469 | // the corner circle with given radius meets the line. 470 | float tan = (float)Math.Abs(Math.Tan(angle)); 471 | float segment = corner.radius / tan; 472 | 473 | // If the segment is larger than the diameter, we need to cap the segment 474 | // to the diameter and reduce the radius to match the segment. This is what 475 | // makes the corner turn radii get smaller as the edge corners get tighter. 476 | if (segment > diameter) 477 | { 478 | segment = diameter; 479 | corner.radius = diameter * tan; 480 | } 481 | 482 | // Calculate both cross points (where the circle touches the p1-cornerPoint line 483 | // and the p2-cornerPoint line). 484 | corner.crossPoint1 = cornerPoint - d1Corner * segment; 485 | corner.crossPoint2 = cornerPoint - d2Corner * segment; 486 | 487 | // Calculation of the coordinates of the circle center. 488 | corner.circleCenter = GetCornerCircleCenter(cornerPoint, corner.crossPoint1, corner.crossPoint2, segment, 489 | corner.radius); 490 | 491 | // Calculate the starting and ending angles. 492 | corner.startAngle = Math.Atan2(corner.crossPoint1.y - corner.circleCenter.y, 493 | corner.crossPoint1.x - corner.circleCenter.x); 494 | corner.endAngle = Math.Atan2(corner.crossPoint2.y - corner.circleCenter.y, 495 | corner.crossPoint2.x - corner.circleCenter.x); 496 | 497 | // Get the full sweep angle from the starting and ending angles. 498 | corner.sweepAngle = corner.endAngle - corner.startAngle; 499 | 500 | // If we are computing the second corner (into the input port), we want to start 501 | // the sweep going backwards. 502 | if (closestPortDirection == Direction.Input) 503 | { 504 | (corner.endAngle, corner.startAngle) = (corner.startAngle, corner.endAngle); 505 | } 506 | 507 | // Validate the sweep angle so it turns into the correct direction. 508 | if (corner.sweepAngle > Math.PI) { corner.sweepAngle = -2 * Math.PI + corner.sweepAngle; } 509 | else if (corner.sweepAngle < -Math.PI) { corner.sweepAngle = 2 * Math.PI + corner.sweepAngle; } 510 | 511 | return corner; 512 | } 513 | 514 | private Vector2 GetCornerCircleCenter(Vector2 cornerPoint, Vector2 crossPoint1, Vector2 crossPoint2, 515 | float segment, float radius) 516 | { 517 | float dx = cornerPoint.x * 2 - crossPoint1.x - crossPoint2.x; 518 | float dy = cornerPoint.y * 2 - crossPoint1.y - crossPoint2.y; 519 | 520 | Vector2 cornerToCenterVector = new(dx, dy); 521 | 522 | float L = cornerToCenterVector.magnitude; 523 | 524 | if (Mathf.Approximately(L, 0)) { return cornerPoint; } 525 | 526 | float d = new Vector2(segment, radius).magnitude; 527 | float factor = d / L; 528 | 529 | return new(cornerPoint.x - cornerToCenterVector.x * factor, 530 | cornerPoint.y - cornerToCenterVector.y * factor); 531 | } 532 | 533 | private void GetRoundedCornerPoints(List points, EdgeCornerSweepValues corner, 534 | Direction closestPortDirection) 535 | { 536 | // Calculate the number of points that will sample the arc from the sweep angle. 537 | int pointsCount = Mathf.CeilToInt((float)Math.Abs(corner.sweepAngle * k_EdgeSweepResampleRatio)); 538 | int sign = Math.Sign(corner.sweepAngle); 539 | bool backwards = closestPortDirection == Direction.Input; 540 | 541 | for (int i = 0; i < pointsCount; ++i) 542 | { 543 | // If we are computing the second corner (into the input port), the sweep is going backwards 544 | // but we still need to add the points to the list in the correct order. 545 | float sweepIndex = backwards ? i - pointsCount : i; 546 | 547 | double sweepedAngle = corner.startAngle + sign * sweepIndex / k_EdgeSweepResampleRatio; 548 | 549 | float pointX = (float)(corner.circleCenter.x + Math.Cos(sweepedAngle) * corner.radius); 550 | float pointY = (float)(corner.circleCenter.y + Math.Sin(sweepedAngle) * corner.radius); 551 | 552 | // Check if we overlap the previous point. If we do, we skip this point so that we 553 | // don't cause the edge polygons to twist. 554 | if (i == 0 && backwards) 555 | { 556 | if (OutputOrientation == Orientation.Horizontal) 557 | { 558 | if (corner.sweepAngle < 0 && points[^1].y > pointY) { continue; } 559 | if (corner.sweepAngle >= 0 && points[^1].y < pointY) { continue; } 560 | } 561 | else 562 | { 563 | if (corner.sweepAngle < 0 && points[^1].x < pointX) { continue; } 564 | if (corner.sweepAngle >= 0 && points[^1].x > pointX) { continue; } 565 | } 566 | } 567 | 568 | points.Add(new(pointX, pointY)); 569 | } 570 | } 571 | 572 | private void AssignControlPoint(ref Vector2 destination, Vector2 newValue) 573 | { 574 | if (!Approximately(destination, newValue)) 575 | { 576 | destination = newValue; 577 | m_RenderPointsDirty = true; 578 | } 579 | } 580 | 581 | protected virtual void ComputeControlPoints() 582 | { 583 | if (m_ControlPointsDirty == false) { return; } 584 | 585 | Profiler.BeginSample("EdgeControl.ComputeControlPoints"); 586 | 587 | float offset = k_EdgeLengthFromPort + k_EdgeTurnDiameter; 588 | 589 | // This is to ensure we don't have the edge extending 590 | // left and right by the offset right when the `from` 591 | // and `to` are on top of each other. 592 | float fromToDistance = (To - From).magnitude; 593 | offset = Mathf.Min(offset, fromToDistance * 2); 594 | offset = Mathf.Max(offset, k_EdgeTurnDiameter); 595 | 596 | if (ControlPoints == null || ControlPoints.Length != 4) { ControlPoints = new Vector2[4]; } 597 | 598 | AssignControlPoint(ref ControlPoints[0], From); 599 | 600 | if (OutputOrientation == Orientation.Horizontal) 601 | { 602 | AssignControlPoint(ref ControlPoints[1], new(From.x + offset, From.y)); 603 | } 604 | else { AssignControlPoint(ref ControlPoints[1], new(From.x, From.y + offset)); } 605 | 606 | if (InputOrientation == Orientation.Horizontal) 607 | { 608 | AssignControlPoint(ref ControlPoints[2], new(To.x - offset, To.y)); 609 | } 610 | else { AssignControlPoint(ref ControlPoints[2], new(To.x, To.y - offset)); } 611 | 612 | AssignControlPoint(ref ControlPoints[3], To); 613 | Profiler.EndSample(); 614 | } 615 | 616 | private void ComputeLayout() 617 | { 618 | Profiler.BeginSample("EdgeControl.ComputeLayout"); 619 | Vector2 to = ControlPoints[^1]; 620 | Vector2 from = ControlPoints[0]; 621 | 622 | Rect rect = new(Vector2.Min(to, from), new(Mathf.Abs(from.x - to.x), Mathf.Abs(from.y - to.y))); 623 | 624 | // Make sure any control points (including tangents, are included in the rect) 625 | for (int i = 1; i < ControlPoints.Length - 1; ++i) 626 | { 627 | if (!rect.Contains(ControlPoints[i])) 628 | { 629 | Vector2 pt = ControlPoints[i]; 630 | rect.xMin = Math.Min(rect.xMin, pt.x); 631 | rect.yMin = Math.Min(rect.yMin, pt.y); 632 | rect.xMax = Math.Max(rect.xMax, pt.x); 633 | rect.yMax = Math.Max(rect.yMax, pt.y); 634 | } 635 | } 636 | 637 | //Make sure that we have the place to display Edges with EdgeControl.k_MinEdgeWidth at the lowest level of zoom. 638 | // float margin = Mathf.Max(EdgeWidth * 0.5f + 1, k_MinEdgeWidth / Graph.minScale); 639 | float 640 | margin = EdgeWidth / 641 | Graph.CurrentScale; //Mathf.Max(EdgeWidth * 0.5f + 1, k_MinEdgeWidth / Graph.minScale); 642 | rect.xMin -= margin; 643 | rect.yMin -= margin; 644 | rect.width += margin; 645 | rect.height += margin; 646 | 647 | if (layout != rect) 648 | { 649 | transform.position = new Vector2(rect.x, rect.y); 650 | style.width = rect.width; 651 | style.height = rect.height; 652 | m_RenderPointsDirty = true; 653 | } 654 | Profiler.EndSample(); 655 | } 656 | 657 | private void OnGenerateVisualContent(MeshGenerationContext mgc) 658 | { 659 | if (EdgeWidth <= 0 || Graph == null) { return; } 660 | 661 | UpdateRenderPoints(); 662 | if (m_RenderPoints.Count == 0) 663 | { 664 | return; // Don't draw anything 665 | } 666 | 667 | // Color outColor = this.outputColor; 668 | Color inColor = InputColor; 669 | 670 | int cpt = m_RenderPoints.Count; 671 | Painter2D painter2D = mgc.painter2D; 672 | 673 | float width = EdgeWidth; 674 | 675 | // float alpha = 1.0f; 676 | float zoom = Graph.CurrentScale; 677 | 678 | if (EdgeWidth * zoom < k_MinEdgeWidth) 679 | { 680 | // alpha = edgeWidth * zoom / k_MinEdgeWidth; 681 | width = k_MinEdgeWidth / zoom; 682 | } 683 | 684 | // k_Gradient.SetKeys(new[]{ new GradientColorKey(outColor, 0),new GradientColorKey(inColor, 1)},new []{new GradientAlphaKey(alpha, 0)}); 685 | painter2D.BeginPath(); 686 | 687 | // painter2D.strokeGradient = k_Gradient; 688 | painter2D.strokeColor = inColor; 689 | painter2D.lineWidth = width; 690 | painter2D.MoveTo(m_RenderPoints[0]); 691 | 692 | for (int i = 1; i < cpt; ++i) { painter2D.LineTo(m_RenderPoints[i]); } 693 | 694 | painter2D.Stroke(); 695 | } 696 | #endregion 697 | 698 | #region Intersection 699 | public override bool ContainsPoint(Vector2 localPoint) 700 | { 701 | Profiler.BeginSample("EdgeControl.ContainsPoint"); 702 | 703 | if (!base.ContainsPoint(localPoint)) 704 | { 705 | Profiler.EndSample(); 706 | return false; 707 | } 708 | 709 | // bounding box check succeeded, do more fine grained check by measuring distance to bezier points 710 | // exclude endpoints 711 | 712 | float capMaxDist = 4 * CapRadius * CapRadius; //(2 * CapRadius)^2 713 | if ((From - localPoint).sqrMagnitude <= capMaxDist || (To - localPoint).sqrMagnitude <= capMaxDist) 714 | { 715 | Profiler.EndSample(); 716 | return false; 717 | } 718 | 719 | List allPoints = m_RenderPoints; 720 | if (allPoints.Count > 0) 721 | { 722 | //we use squareDistance to avoid sqrts 723 | float distance = (allPoints[0] - localPoint).sqrMagnitude; 724 | float interceptWidth2 = InterceptWidth * InterceptWidth; 725 | for (int i = 0; i < allPoints.Count - 1; i++) 726 | { 727 | Vector2 currentPoint = allPoints[i]; 728 | Vector2 nextPoint = allPoints[i + 1]; 729 | 730 | Vector2 next2Current = nextPoint - currentPoint; 731 | float distanceNext = (nextPoint - localPoint).sqrMagnitude; 732 | float distanceLine = next2Current.sqrMagnitude; 733 | 734 | // if the point is somewhere between the two points 735 | if (distance < distanceLine && distanceNext < distanceLine) 736 | { 737 | //https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line 738 | float d = next2Current.y * localPoint.x - 739 | next2Current.x * localPoint.y + nextPoint.x * currentPoint.y - 740 | nextPoint.y * currentPoint.x; 741 | if (d * d < interceptWidth2 * distanceLine) 742 | { 743 | Profiler.EndSample(); 744 | return true; 745 | } 746 | } 747 | 748 | distance = distanceNext; 749 | } 750 | } 751 | 752 | Profiler.EndSample(); 753 | return false; 754 | } 755 | 756 | public override bool Overlaps(Rect rect) 757 | { 758 | if (base.Overlaps(rect)) 759 | { 760 | for (int a = 0; a < m_RenderPoints.Count - 1; a++) 761 | { 762 | if (RectUtils.IntersectsSegment(rect, m_RenderPoints[a], m_RenderPoints[a + 1])) { return true; } 763 | } 764 | } 765 | return false; 766 | } 767 | #endregion 768 | 769 | #region Event Handlers 770 | protected override void OnCustomStyleResolved(ICustomStyle styles) 771 | { 772 | base.OnCustomStyleResolved(styles); 773 | 774 | if (styles.TryGetValue(s_EdgeWidthProperty, out int edgeWidthValue)) { EdgeWidth = edgeWidthValue; } 775 | if (styles.TryGetValue(s_EdgeWidthSelectedProperty, out int edgeWidthSelectedValue)) 776 | { 777 | EdgeWidthSelected = edgeWidthSelectedValue; 778 | } 779 | if (styles.TryGetValue(s_EdgeColorSelectedProperty, out Color selectColorValue)) 780 | { 781 | ColorSelected = selectColorValue; 782 | } 783 | if (styles.TryGetValue(s_EdgeColorProperty, out Color edgeColorValue)) { ColorUnselected = edgeColorValue; } 784 | } 785 | 786 | protected override void OnAddedToGraphView() 787 | { 788 | base.OnAddedToGraphView(); 789 | Graph.OnViewTransformChanged += MarkDirtyOnTransformChanged; 790 | OnEdgeChanged(); 791 | } 792 | 793 | protected override void OnRemovedFromGraphView() 794 | { 795 | base.OnRemovedFromGraphView(); 796 | Graph.OnViewTransformChanged -= MarkDirtyOnTransformChanged; 797 | } 798 | 799 | private void MarkDirtyOnTransformChanged(GraphView gv) { MarkDirtyRepaint(); } 800 | 801 | protected override void OnEdgeChanged() 802 | { 803 | DrawFromCap = Output == null; 804 | DrawToCap = Input == null; 805 | m_ControlPointsDirty = true; 806 | UpdateLayout(); 807 | } 808 | #endregion 809 | 810 | #region Helper Classes/Structs 811 | private struct EdgeCornerSweepValues 812 | { 813 | public Vector2 circleCenter; 814 | public double sweepAngle; 815 | public double startAngle; 816 | public double endAngle; 817 | public Vector2 crossPoint1; 818 | public Vector2 crossPoint2; 819 | public float radius; 820 | } 821 | #endregion 822 | 823 | public override string ToString() => $"Output({Output}) -> Input({Input})"; 824 | } 825 | } -------------------------------------------------------------------------------- /Elements/Graph/GraphElement.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using System; 6 | using UnityEngine; 7 | using UnityEngine.UIElements; 8 | 9 | namespace GraphViewPlayer 10 | { 11 | public abstract class GraphElement : VisualElement, ISelectable, IPositionable 12 | { 13 | private static readonly CustomStyleProperty s_LayerProperty = new("--layer"); 14 | private GraphView m_GraphView; 15 | 16 | private int m_Layer; 17 | private bool m_LayerIsInline; 18 | private bool m_Selected; 19 | 20 | protected GraphElement() 21 | { 22 | ClearClassList(); 23 | AddToClassList("graph-element"); 24 | RegisterCallback(OnCustomStyleResolved); 25 | 26 | // Setup Manipulators 27 | this.AddManipulator(new SelectableManipulator()); 28 | } 29 | 30 | public GraphView Graph 31 | { 32 | get => m_GraphView; 33 | internal set 34 | { 35 | if (m_GraphView == value) { return; } 36 | 37 | // We want m_GraphView there whenever these events are call so we can do setup/teardown 38 | if (value == null) 39 | { 40 | OnRemovedFromGraphView(); 41 | m_GraphView = null; 42 | } 43 | else 44 | { 45 | m_GraphView = value; 46 | OnAddedToGraphView(); 47 | } 48 | } 49 | } 50 | 51 | public virtual string Title 52 | { 53 | get => name; 54 | set => throw new NotImplementedException(); 55 | } 56 | 57 | public Capabilities Capabilities { get; set; } 58 | 59 | #region Graph Events 60 | protected virtual void OnAddedToGraphView() { } 61 | 62 | protected virtual void OnRemovedFromGraphView() 63 | { 64 | pickingMode = PickingMode.Position; 65 | Selected = false; 66 | ResetLayer(); 67 | } 68 | #endregion 69 | 70 | #region Selectable 71 | public ISelector Selector => Graph.ContentContainer; 72 | 73 | public virtual bool Selected 74 | { 75 | get => m_Selected; 76 | set 77 | { 78 | if (m_Selected == value) { return; } 79 | if (value && IsSelectable()) 80 | { 81 | m_Selected = true; 82 | if (IsAscendable() && resolvedStyle.position != Position.Relative) { BringToFront(); } 83 | } 84 | else 85 | { 86 | m_Selected = false; 87 | if (Graph != null) Graph.ContentContainer.RemoveFromDragSelection(this); 88 | } 89 | } 90 | } 91 | #endregion 92 | 93 | #region Layer 94 | public int Layer 95 | { 96 | get => m_Layer; 97 | set 98 | { 99 | if (m_Layer == value) { return; } 100 | m_Layer = value; 101 | m_LayerIsInline = true; 102 | Graph?.ChangeLayer(this); 103 | } 104 | } 105 | 106 | public void ResetLayer() 107 | { 108 | m_LayerIsInline = false; 109 | customStyle.TryGetValue(s_LayerProperty, out int styleLayer); 110 | Layer = styleLayer; 111 | } 112 | #endregion 113 | 114 | #region Custom Style 115 | private void OnCustomStyleResolved(CustomStyleResolvedEvent e) { OnCustomStyleResolved(e.customStyle); } 116 | 117 | protected virtual void OnCustomStyleResolved(ICustomStyle styleOverride) 118 | { 119 | if (!m_LayerIsInline) { ResetLayer(); } 120 | } 121 | #endregion 122 | 123 | #region Position 124 | public virtual event Action OnPositionChange; 125 | public virtual Vector2 GetGlobalCenter() => Graph.ContentContainer.LocalToWorld(GetCenter()); 126 | public virtual Vector2 GetCenter() => layout.center + (Vector2)transform.position; 127 | public virtual Vector2 GetPosition() => transform.position; 128 | 129 | public virtual void SetPosition(Vector2 newPosition) 130 | { 131 | transform.position = newPosition; 132 | OnPositionChange?.Invoke(new() 133 | { 134 | element = this, 135 | position = newPosition 136 | }); 137 | } 138 | 139 | public virtual void ApplyDeltaToPosition(Vector2 delta) 140 | { 141 | SetPosition((Vector2)transform.position + delta); 142 | } 143 | #endregion 144 | 145 | #region Capabilities 146 | public virtual bool IsMovable() => (Capabilities & Capabilities.Movable) == Capabilities.Movable; 147 | public virtual bool IsDroppable() => (Capabilities & Capabilities.Droppable) == Capabilities.Droppable; 148 | public virtual bool IsAscendable() => (Capabilities & Capabilities.Ascendable) == Capabilities.Ascendable; 149 | public virtual bool IsRenamable() => (Capabilities & Capabilities.Renamable) == Capabilities.Renamable; 150 | public virtual bool IsCopiable() => (Capabilities & Capabilities.Copiable) == Capabilities.Copiable; 151 | public virtual bool IsSnappable() => (Capabilities & Capabilities.Snappable) == Capabilities.Snappable; 152 | public virtual bool IsResizable() => false; 153 | public virtual bool IsGroupable() => false; 154 | public virtual bool IsStackable() => false; 155 | 156 | public virtual bool IsSelectable() => 157 | (Capabilities & Capabilities.Selectable) == Capabilities.Selectable && visible; 158 | #endregion 159 | } 160 | } -------------------------------------------------------------------------------- /Elements/GraphElementContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.UIElements; 5 | 6 | namespace GraphViewPlayer 7 | { 8 | /// 9 | /// ContentView houses all graph elements and is the element directly scaled/panned by the GraphView. 10 | /// ContentView also handles selection based drag and drop for all GraphElements. 11 | /// 12 | public class GraphElementContainer : VisualElement, ISelector, IPositionable, IDropPayload 13 | { 14 | private readonly List m_Selection; 15 | 16 | private GraphView m_GraphView; 17 | private GraphElement m_Dragged; 18 | 19 | public GraphElementContainer(GraphView graphView) 20 | { 21 | AddToClassList("content-view-container"); 22 | pickingMode = PickingMode.Ignore; 23 | usageHints = UsageHints.GroupTransform; 24 | 25 | // Selection 26 | m_Selection = new(); 27 | 28 | // Graph View 29 | m_GraphView = graphView; 30 | 31 | // Cached Queries 32 | ElementsAll = this.Query().Build(); 33 | ElementsSelected = this.Query().Where(WhereSelected).Build(); 34 | ElementsUnselected = this.Query().Where(WhereUnselected).Build(); 35 | Nodes = this.Query().Build(); 36 | NodesSelected = this.Query().Where(WhereSelected).Build(); 37 | Edges = this.Query().Build(); 38 | EdgesSelected = this.Query().Where(WhereSelected).Build(); 39 | Ports = this.Query().Build(); 40 | 41 | // Event Handlers 42 | RegisterCallback(OnDragOffer); 43 | RegisterCallback(OnDragBegin); 44 | RegisterCallback(OnDrag); 45 | RegisterCallback(OnDragEnd); 46 | RegisterCallback(OnDragCancel); 47 | } 48 | 49 | #region Selection 50 | public UQueryState Nodes { get; } 51 | public UQueryState NodesSelected { get; } 52 | public UQueryState Ports { get; } 53 | public UQueryState Edges { get; } 54 | public UQueryState EdgesSelected { get; } 55 | public UQueryState ElementsAll { get; } 56 | public UQueryState ElementsSelected { get; } 57 | public UQueryState ElementsUnselected { get; } 58 | 59 | public void SelectAll() 60 | { 61 | foreach (GraphElement ge in ElementsAll) { ge.Selected = true; } 62 | } 63 | 64 | public void ClearSelection() 65 | { 66 | foreach (GraphElement ge in ElementsAll) { ge.Selected = false; } 67 | } 68 | 69 | public void CollectAll(List toPopulate) 70 | { 71 | foreach (GraphElement ge in ElementsAll) { toPopulate.Add(ge); } 72 | } 73 | 74 | public void CollectSelected(List toPopulate) 75 | { 76 | foreach (GraphElement ge in ElementsAll) 77 | { 78 | if (ge.Selected) { toPopulate.Add(ge); } 79 | } 80 | } 81 | 82 | public void CollectUnselected(List toPopulate) 83 | { 84 | foreach (GraphElement ge in ElementsAll) 85 | { 86 | if (!ge.Selected) { toPopulate.Add(ge); } 87 | } 88 | } 89 | 90 | public void ForEachAll(Action action) { ElementsAll.ForEach(action); } 91 | public void ForEachSelected(Action action) { ElementsSelected.ForEach(action); } 92 | public void ForEachUnselected(Action action) { ElementsUnselected.ForEach(action); } 93 | 94 | private bool WhereSelected(ISelectable selectable) => selectable.Selected; 95 | private bool WhereUnselected(ISelectable selectable) => !selectable.Selected; 96 | #endregion 97 | 98 | #region Drag and Drop 99 | public void RemoveFromDragSelection(GraphElement e) => m_Selection.Remove(e); 100 | 101 | private void OnDragOffer(DragOfferEvent e) 102 | { 103 | // Verify the target can handle the drag 104 | if (e.GetDraggedElement() is not GraphElement graphElement) { return;} 105 | 106 | // Consume the drag event 107 | e.StopImmediatePropagation(); 108 | 109 | // Store draggable 110 | m_Dragged = graphElement; 111 | 112 | // Replace the drag acceptor 113 | e.AcceptDrag(this); 114 | } 115 | 116 | private void OnDragBegin(DragBeginEvent e) 117 | { 118 | // If we didn't accept a drag offer, exit. This should never happen 119 | if (m_Dragged == null) throw new("Unexpected drag begin event"); 120 | 121 | // Swallow event 122 | e.StopImmediatePropagation(); 123 | 124 | // Track for panning 125 | m_GraphView.TrackElementForPan(this); 126 | 127 | // Handle drag begin 128 | if (m_Dragged is BaseEdge edge) HandleDragBegin(e, edge); 129 | else if (m_Dragged is BaseNode node) HandleDragBegin(e, node); 130 | } 131 | 132 | private void HandleDragBegin(DragBeginEvent e, BaseEdge draggedEdge) 133 | { 134 | // Determine which end of the edge was dragged 135 | bool draggedInput; 136 | if (draggedEdge.Input == null) { draggedInput = true; } 137 | else if (draggedEdge.Output == null) { draggedInput = false; } 138 | else 139 | { 140 | // Vector2 mousePosition = this.WorldToLocal(e.mousePosition); 141 | Vector2 inputPos = draggedEdge.Input.GetGlobalCenter(); 142 | Vector2 outputPos = draggedEdge.Output.GetGlobalCenter(); 143 | float distanceFromInput = (e.mousePosition - inputPos).sqrMagnitude; 144 | float distanceFromOutput = (e.mousePosition - outputPos).sqrMagnitude; 145 | draggedInput = distanceFromInput < distanceFromOutput; 146 | } 147 | 148 | // Collect selection and set them to drag mode 149 | BasePort draggedPort; 150 | BasePort anchoredPort; 151 | if (draggedInput) 152 | { 153 | draggedPort = draggedEdge.Input; 154 | anchoredPort = draggedEdge.Output; 155 | HandleDragBeginEdgeHelper(draggedPort, draggedEdge, SetDraggedEdgeInputOverride); 156 | } 157 | else 158 | { 159 | draggedPort = draggedEdge.Output; 160 | anchoredPort = draggedEdge.Input; 161 | HandleDragBeginEdgeHelper(draggedPort, draggedEdge, SetDraggedEdgeOutputOverride); 162 | } 163 | m_GraphView.IlluminateCompatiblePorts(anchoredPort); 164 | 165 | // Set user data 166 | e.SetUserData(this); 167 | } 168 | 169 | private void HandleDragBeginEdgeHelper( 170 | BasePort draggedPort, BaseEdge draggedEdge, Action positionAction) 171 | { 172 | if (draggedPort != null && draggedPort.AllowMultiDrag) 173 | { 174 | foreach (BaseEdge edge in draggedPort.Connections) 175 | { 176 | if (edge.Selected) 177 | { 178 | positionAction(edge); 179 | edge.pickingMode = PickingMode.Ignore; 180 | edge.visible = true; 181 | m_Selection.Add(edge); 182 | } 183 | } 184 | } 185 | else 186 | { 187 | positionAction(draggedEdge); 188 | draggedEdge.pickingMode = PickingMode.Ignore; 189 | draggedEdge.visible = true; 190 | m_Selection.Add(draggedEdge); 191 | } 192 | } 193 | private void SetDraggedEdgeInputOverride(BaseEdge edge) 194 | => edge.SetInputPositionOverride(edge.GetInputPositionOverride()); 195 | private void SetDraggedEdgeOutputOverride(BaseEdge edge) 196 | => edge.SetOutputPositionOverride(edge.GetOutputPositionOverride()); 197 | 198 | private void HandleDragBegin(DragBeginEvent e, BaseNode draggedNode) 199 | { 200 | // Collect selection 201 | foreach (BaseNode node in NodesSelected) 202 | { 203 | // Add to selection 204 | m_Selection.Add(node); 205 | 206 | // Disable picking 207 | node.pickingMode = PickingMode.Ignore; 208 | } 209 | } 210 | 211 | private void OnDrag(DragEvent e) 212 | { 213 | // Swallow event 214 | e.StopImmediatePropagation(); 215 | 216 | // Handle drag 217 | if (m_Dragged is BaseEdge edge) HandleDrag(e, edge); 218 | else if (m_Dragged is BaseNode node) HandleDrag(e, node); 219 | } 220 | 221 | private void HandleDrag(DragEvent e, BaseNode draggedNode) 222 | { 223 | for (int i = 0; i < m_Selection.Count; i++) 224 | { 225 | BaseNode node = (BaseNode) m_Selection[i]; 226 | node.ApplyDeltaToPosition(e.mouseDelta / transform.scale); 227 | } 228 | } 229 | 230 | private void HandleDrag(DragEvent e, BaseEdge draggedEdge) 231 | { 232 | Vector2 newPosition = this.WorldToLocal(e.mousePosition); 233 | for (int i = 0; i < m_Selection.Count; i++) 234 | { 235 | BaseEdge edge = (BaseEdge) m_Selection[i]; 236 | if (edge.IsInputPositionOverriden()) { edge.SetInputPositionOverride(newPosition); } 237 | else { edge.SetOutputPositionOverride(newPosition); } 238 | } 239 | } 240 | 241 | private void OnDragEnd(DragEndEvent e) 242 | { 243 | // Swallow event 244 | e.StopImmediatePropagation(); 245 | 246 | // Untrack for panning 247 | m_GraphView.UntrackElementForPan(this); 248 | 249 | // Handle drag end 250 | if (m_Dragged is BaseEdge edge) HandleDragEnd(e, edge); 251 | else if (m_Dragged is BaseNode node) HandleDragEnd(e, node); 252 | 253 | // Reset 254 | Reset(); 255 | } 256 | 257 | private void HandleDragEnd(DragEndEvent e, BaseNode draggedNode) 258 | { 259 | for (int i = 0; i < m_Selection.Count; i++) 260 | { 261 | BaseNode node = (BaseNode) m_Selection[i]; 262 | // Skip deleted nodes 263 | // if (node.parent == null) continue; 264 | // Re-enable picking 265 | node.pickingMode = PickingMode.Position; 266 | } 267 | } 268 | 269 | private void HandleDragEnd(DragEndEvent e, BaseEdge draggedEdge) 270 | { 271 | for (int i = 0; i < m_Selection.Count; i++) 272 | { 273 | BaseEdge edge = (BaseEdge) m_Selection[i]; 274 | // Skip deleted edges 275 | // if (edge.parent == null) continue; 276 | // Re-enable picking 277 | edge.pickingMode = PickingMode.Position; 278 | } 279 | // Reset ports 280 | m_GraphView.IlluminateAllPorts(); 281 | } 282 | 283 | private void OnDragCancel(DragCancelEvent e) 284 | { 285 | // Swallow event 286 | e.StopImmediatePropagation(); 287 | 288 | // Untrack for panning 289 | Vector2 totalDiff = (e.DeltaToDragOrigin - m_GraphView.UntrackElementForPan(this, true)) / transform.scale; 290 | 291 | // Handle drag cancel 292 | if (m_Dragged is BaseEdge edge) HandleDragCancel(e, edge, totalDiff); 293 | else if (m_Dragged is BaseNode node) HandleDragCancel(e, node, totalDiff); 294 | 295 | // Reset 296 | Reset(); 297 | } 298 | 299 | private void HandleDragCancel(DragCancelEvent e, BaseNode draggedNode, Vector2 totalDragDiff) 300 | { 301 | for (int i = 0; i < m_Selection.Count; i++) 302 | { 303 | // Grab node 304 | BaseNode node = (BaseNode) m_Selection[i]; 305 | // Skip deleted nodes 306 | // if (node.parent == null) continue; 307 | // Re-enable picking 308 | node.pickingMode = PickingMode.Position; 309 | // Reset position before drag 310 | node.ApplyDeltaToPosition(totalDragDiff); 311 | } 312 | } 313 | 314 | private void HandleDragCancel(DragCancelEvent e, BaseEdge draggedEdge, Vector2 totalDragDiff) 315 | { 316 | // Reverse iteration because we alter the collection as we go 317 | for (int i = m_Selection.Count - 1; i >= 0; i--) 318 | { 319 | BaseEdge edge = (BaseEdge) m_Selection[i]; 320 | // Skip deleted edges 321 | // if (edge.parent == null) continue; 322 | // Re-enable picking 323 | edge.pickingMode = PickingMode.Position; 324 | // Reset position to before drag if we're dragging an existing edge 325 | if (edge.IsRealEdge()) 326 | { 327 | edge.ResetLayer(); 328 | edge.UnsetPositionOverrides(); 329 | edge.Selected = false; 330 | } 331 | // Otherwise this is a candidate edge dragged from a port and should be DESTROYED!!! 332 | else m_GraphView.RemoveElement(edge); 333 | } 334 | // Reset ports 335 | m_GraphView.IlluminateAllPorts(); 336 | } 337 | 338 | private void Reset() 339 | { 340 | m_Dragged = null; 341 | m_Selection.Clear(); 342 | } 343 | #endregion 344 | 345 | #region Position 346 | public event Action OnPositionChange; 347 | public Vector2 GetGlobalCenter() => m_Dragged.GetGlobalCenter(); 348 | public Vector2 GetCenter() => m_Dragged.GetCenter(); 349 | public Vector2 GetPosition() => m_Dragged.GetPosition(); 350 | public void SetPosition(Vector2 position) 351 | { 352 | for (int i = 0; i < m_Selection.Count; i++) 353 | { 354 | GraphElement element = m_Selection[i]; 355 | element.SetPosition(position); 356 | } 357 | OnPositionChange?.Invoke(new()); 358 | } 359 | public void ApplyDeltaToPosition(Vector2 delta) 360 | { 361 | for (int i = 0; i < m_Selection.Count; i++) 362 | { 363 | GraphElement element = m_Selection[i]; 364 | element.ApplyDeltaToPosition(delta); 365 | } 366 | OnPositionChange?.Invoke(new()); 367 | } 368 | #endregion 369 | 370 | #region Drop Payload 371 | public Type GetPayloadType() => m_Dragged.GetType(); 372 | public IReadOnlyList GetPayload() => m_Selection; 373 | #endregion 374 | } 375 | 376 | public interface IDropPayload 377 | { 378 | public Type GetPayloadType(); 379 | public IReadOnlyList GetPayload(); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /Elements/GraphView.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Runtime.CompilerServices; 9 | using UnityEngine; 10 | using UnityEngine.UIElements; 11 | 12 | namespace GraphViewPlayer 13 | { 14 | public abstract class GraphView : VisualElement 15 | { 16 | private const int k_FrameBorder = 30; 17 | private const int k_PanAreaWidth = 100; 18 | private const int k_PanSpeed = 4; 19 | private const int k_PanInterval = 10; 20 | private const float k_MinSpeedFactor = 0.5f; 21 | private const float k_MaxSpeedFactor = 2.5f; 22 | private const float k_MaxPanSpeed = k_MaxSpeedFactor * k_PanSpeed; 23 | private const float k_PanAreaWidthAndMinSpeedFactor = k_PanAreaWidth + k_MinSpeedFactor; 24 | 25 | private static StyleSheet s_DefaultStyle; 26 | 27 | private readonly Dictionary m_ContainerLayers; 28 | private readonly VisualElement m_GridBackground; 29 | 30 | #region Constructor 31 | protected GraphView() 32 | { 33 | // 34 | // GraphView - Level 0 35 | // 36 | // Style 37 | styleSheets.Add(DefaultStyle); 38 | AddToClassList("graph-view"); 39 | usageHints = UsageHints.MaskContainer; // Should equate to RenderHints.ClipWithScissors 40 | 41 | // Manipulators 42 | m_Zoomer = new(); 43 | this.AddManipulator(m_Zoomer); 44 | this.AddManipulator(new DragAndDropManipulator()); 45 | 46 | // 47 | // Graph View Container & Grid - Level 1 48 | // 49 | // Root Container 50 | m_GridBackground = new GridBackground(); 51 | hierarchy.Add(m_GridBackground); 52 | 53 | // 54 | // Content Container - Level 2 55 | // 56 | // Content Container 57 | ContentContainer = new(this); 58 | m_GridBackground.Add(ContentContainer); 59 | 60 | // 61 | // Other Initialization 62 | // 63 | // Layers 64 | m_ContainerLayers = new(); 65 | 66 | // Create Marquee 67 | m_Marquee = new(); 68 | 69 | // Panning 70 | m_PanSchedule = schedule 71 | .Execute(Pan) 72 | .Every(k_PanInterval) 73 | .StartingIn(k_PanInterval); 74 | m_PanSchedule.Pause(); 75 | 76 | // Focus 77 | focusable = true; 78 | Focus(); 79 | } 80 | #endregion 81 | 82 | #region Helper Classes 83 | public class Layer : VisualElement 84 | { 85 | public Layer() => pickingMode = PickingMode.Ignore; 86 | } 87 | #endregion 88 | 89 | #region Properties 90 | private static StyleSheet DefaultStyle 91 | { 92 | get 93 | { 94 | if (s_DefaultStyle == null) 95 | { 96 | s_DefaultStyle = Resources.Load("GraphViewPlayer/GraphView"); 97 | } 98 | return s_DefaultStyle; 99 | } 100 | } 101 | 102 | internal ViewTransformChanged OnViewTransformChanged { get; set; } 103 | 104 | protected internal GraphElementContainer ContentContainer { get; } 105 | internal ITransform ViewTransform => ContentContainer.transform; 106 | #endregion 107 | 108 | #region Factories 109 | public virtual BaseEdge CreateEdge() => new Edge(); 110 | 111 | public virtual BasePort CreatePort(Orientation orientation, Direction direction, PortCapacity capacity) 112 | => new(orientation, direction, capacity); 113 | #endregion 114 | 115 | #region View Transform 116 | internal delegate void ViewTransformChanged(GraphView graphView); 117 | 118 | public void UpdateViewTransform(Vector3 newPosition) 119 | => UpdateViewTransform(newPosition, ViewTransform.scale); 120 | 121 | public void UpdateViewTransform(Vector3 newPosition, Vector3 newScale) 122 | { 123 | float validateFloat = newPosition.x + newPosition.y + newPosition.z + newScale.x + newScale.y + newScale.z; 124 | if (float.IsInfinity(validateFloat) || float.IsNaN(validateFloat)) { return; } 125 | 126 | ViewTransform.scale = newScale; 127 | ViewTransform.position = newPosition; 128 | 129 | OnViewTransformChanged?.Invoke(this); 130 | OnViewportChanged(); 131 | } 132 | #endregion 133 | 134 | #region Pan 135 | private IPositionable m_PanElement; 136 | private Vector2 m_PanOriginDiff; 137 | private readonly IVisualElementScheduledItem m_PanSchedule; 138 | 139 | internal void TrackElementForPan(IPositionable element) 140 | { 141 | m_PanOriginDiff = Vector2.zero; 142 | m_PanElement = element; 143 | m_PanSchedule.Resume(); 144 | } 145 | 146 | internal Vector2 UntrackElementForPan(IPositionable element, bool resetView = false) 147 | { 148 | if (element == m_PanElement) 149 | { 150 | m_PanSchedule.Pause(); 151 | m_PanElement = null; 152 | if (resetView) { UpdateViewTransform((Vector2)ViewTransform.position + m_PanOriginDiff); } 153 | return m_PanOriginDiff; 154 | } 155 | return Vector2.zero; 156 | } 157 | 158 | private Vector2 GetEffectivePanSpeed(Vector2 mousePos) 159 | { 160 | Vector2 effectiveSpeed = Vector2.zero; 161 | 162 | if (mousePos.x <= k_PanAreaWidth) 163 | { 164 | effectiveSpeed.x = -((k_PanAreaWidth - mousePos.x) / k_PanAreaWidthAndMinSpeedFactor) * k_PanSpeed; 165 | } 166 | else if (mousePos.x >= m_GridBackground.layout.width - k_PanAreaWidth) 167 | { 168 | effectiveSpeed.x = (mousePos.x - (m_GridBackground.layout.width - k_PanAreaWidth)) 169 | / k_PanAreaWidthAndMinSpeedFactor * k_PanSpeed; 170 | } 171 | 172 | if (mousePos.y <= k_PanAreaWidth) 173 | { 174 | effectiveSpeed.y = -((k_PanAreaWidth - mousePos.y) / k_PanAreaWidthAndMinSpeedFactor) * k_PanSpeed; 175 | } 176 | else if (mousePos.y >= m_GridBackground.layout.height - k_PanAreaWidth) 177 | { 178 | effectiveSpeed.y = (mousePos.y - (m_GridBackground.layout.height - k_PanAreaWidth)) 179 | / k_PanAreaWidthAndMinSpeedFactor * k_PanSpeed; 180 | } 181 | 182 | return Vector2.ClampMagnitude(effectiveSpeed, k_MaxPanSpeed); 183 | } 184 | 185 | private void Pan() 186 | { 187 | // Use element center as the point to test against the bounds of the pan area 188 | Vector2 elementPositionWorld = m_PanElement.GetGlobalCenter(); 189 | 190 | // If the point has entered the bounds of the pan area, calculate how fast we want to pan 191 | Vector2 speed = GetEffectivePanSpeed(elementPositionWorld); 192 | if (Vector2.zero == speed) { return; } 193 | 194 | // Record changes in pan 195 | m_PanOriginDiff += speed; 196 | 197 | // Update the view transform (to pan) 198 | UpdateViewTransform((Vector2)ViewTransform.position - speed); 199 | 200 | // Speed is scaled according to the current zoom level 201 | Vector2 localSpeed = speed / CurrentScale; 202 | 203 | // Set position 204 | m_PanElement.ApplyDeltaToPosition(localSpeed); 205 | } 206 | #endregion 207 | 208 | #region Layers 209 | internal void ChangeLayer(GraphElement element) { GetLayer(element.Layer).Add(element); } 210 | 211 | private VisualElement GetLayer(int index) 212 | { 213 | if (!m_ContainerLayers.TryGetValue(index, out Layer layer)) 214 | { 215 | layer = new() { name = $"Layer {index}" }; 216 | m_ContainerLayers[index] = layer; 217 | 218 | // TODO 219 | int indexOfLayer = m_ContainerLayers.OrderBy(t => t.Key).Select(t => t.Value).ToList().IndexOf(layer); 220 | ContentContainer.Insert(indexOfLayer, layer); 221 | } 222 | return layer; 223 | } 224 | #endregion 225 | 226 | #region Zoom 227 | private readonly ZoomManipulator m_Zoomer; 228 | 229 | public float MinScale 230 | { 231 | get => m_Zoomer.MinScale; 232 | set 233 | { 234 | m_Zoomer.MinScale = Math.Min(value, ZoomManipulator.DefaultMinScale); 235 | ValidateTransform(); 236 | } 237 | } 238 | 239 | public float MaxScale 240 | { 241 | get => m_Zoomer.MaxScale; 242 | set 243 | { 244 | m_Zoomer.MaxScale = Math.Max(value, ZoomManipulator.DefaultMaxScale); 245 | ValidateTransform(); 246 | } 247 | } 248 | 249 | public float ScaleStep 250 | { 251 | get => m_Zoomer.ScaleStep; 252 | set 253 | { 254 | m_Zoomer.ScaleStep = Math.Min(value, (MaxScale - MinScale) / 2); 255 | ValidateTransform(); 256 | } 257 | } 258 | 259 | public float ReferenceScale 260 | { 261 | get => m_Zoomer.ReferenceScale; 262 | set 263 | { 264 | m_Zoomer.ReferenceScale = Math.Clamp(value, MinScale, MaxScale); 265 | ValidateTransform(); 266 | } 267 | } 268 | 269 | public float CurrentScale => ViewTransform.scale.x; 270 | 271 | protected void ValidateTransform() 272 | { 273 | if (ContentContainer == null) { return; } 274 | Vector3 transformScale = ViewTransform.scale; 275 | 276 | transformScale.x = Mathf.Clamp(transformScale.x, MinScale, MaxScale); 277 | transformScale.y = Mathf.Clamp(transformScale.y, MinScale, MaxScale); 278 | 279 | UpdateViewTransform(ViewTransform.position, transformScale); 280 | } 281 | #endregion 282 | 283 | #region Event Handlers 284 | private readonly Marquee m_Marquee; 285 | private bool m_DraggingView; 286 | private bool m_DraggingMarquee; 287 | 288 | [EventInterest(typeof(DragOfferEvent), typeof(DragEvent), typeof(DragEndEvent), typeof(DragCancelEvent), 289 | typeof(DropEnterEvent), typeof(DropEvent), typeof(DropExitEvent))] 290 | protected override void ExecuteDefaultActionAtTarget(EventBase evt) 291 | { 292 | base.ExecuteDefaultActionAtTarget(evt); 293 | if (evt.eventTypeId == DragOfferEvent.TypeId()) { OnDragOffer((DragOfferEvent)evt); } 294 | else if (evt.eventTypeId == DragEvent.TypeId()) { OnDrag((DragEvent)evt); } 295 | else if (evt.eventTypeId == DragEndEvent.TypeId()) { OnDragEnd((DragEndEvent)evt); } 296 | else if (evt.eventTypeId == DragCancelEvent.TypeId()) { OnDragCancel((DragCancelEvent)evt); } 297 | else if (evt.eventTypeId == DropEnterEvent.TypeId()) { OnDropEnter((DropEnterEvent)evt); } 298 | else if (evt.eventTypeId == DropEvent.TypeId()) { OnDrop((DropEvent)evt); } 299 | else if (evt.eventTypeId == DropExitEvent.TypeId()) { OnDropExit((DropExitEvent)evt); } 300 | } 301 | 302 | private void OnDragOffer(DragOfferEvent e) 303 | { 304 | if (IsViewDrag(e)) 305 | { 306 | // Accept Drag 307 | e.AcceptDrag(this); 308 | e.StopImmediatePropagation(); 309 | m_DraggingView = true; 310 | 311 | // Assume focus 312 | Focus(); 313 | } 314 | else if (IsMarqueeDrag(e)) 315 | { 316 | // Accept Drag 317 | e.AcceptDrag(this); 318 | e.StopImmediatePropagation(); 319 | m_DraggingMarquee = true; 320 | 321 | // Clear selection if this is an exclusive select 322 | bool additive = e.modifiers.IsShift(); 323 | bool subtractive = e.modifiers.IsActionKey(); 324 | bool exclusive = !(additive ^ subtractive); 325 | if (exclusive) { ContentContainer.ClearSelection(); } 326 | 327 | // Create marquee 328 | Add(m_Marquee); 329 | Vector2 position = this.WorldToLocal(e.mousePosition); 330 | m_Marquee.Coordinates = new() 331 | { 332 | start = position, 333 | end = position 334 | }; 335 | 336 | // Assume focus 337 | Focus(); 338 | } 339 | } 340 | 341 | private void OnDrag(DragEvent e) 342 | { 343 | if (m_DraggingMarquee) 344 | { 345 | e.StopImmediatePropagation(); 346 | 347 | // TODO - MouseMoveEvent doesn't correctly report mouse button so I don't check IsMarqueeDrag 348 | m_Marquee.End = this.WorldToLocal(e.mousePosition); 349 | } 350 | else if (m_DraggingView) 351 | { 352 | e.StopImmediatePropagation(); 353 | 354 | // TODO - MouseMoveEvent doesn't correctly report mouse button so I don't check IsViewDrag 355 | UpdateViewTransform(ViewTransform.position + (Vector3)e.mouseDelta); 356 | } 357 | } 358 | 359 | private void OnDragEnd(DragEndEvent e) 360 | { 361 | if (m_DraggingMarquee) 362 | { 363 | // Remove marquee 364 | e.StopImmediatePropagation(); 365 | m_Marquee.RemoveFromHierarchy(); 366 | 367 | // Ignore if the rectangle is infinitely small 368 | Rect selectionRect = m_Marquee.SelectionRect; 369 | if (selectionRect.size == Vector2.zero) { return; } 370 | 371 | // Select elements that overlap the marquee 372 | bool additive = e.modifiers.IsShift(); 373 | bool subtractive = e.modifiers.IsActionKey(); 374 | bool exclusive = !(additive ^ subtractive); 375 | foreach (GraphElement element in ContentContainer.ElementsAll) 376 | { 377 | Rect localSelRect = this.ChangeCoordinatesTo(element, selectionRect); 378 | if (element.Overlaps(localSelRect)) { element.Selected = exclusive || additive; } 379 | else if (exclusive) { element.Selected = false; } 380 | } 381 | m_DraggingMarquee = false; 382 | } 383 | else if (m_DraggingView) 384 | { 385 | e.StopImmediatePropagation(); 386 | m_DraggingView = false; 387 | } 388 | } 389 | 390 | private void OnDragCancel(DragCancelEvent e) 391 | { 392 | if (m_DraggingMarquee) 393 | { 394 | e.StopImmediatePropagation(); 395 | m_Marquee.RemoveFromHierarchy(); 396 | m_DraggingMarquee = false; 397 | } 398 | else if (m_DraggingView) 399 | { 400 | e.StopImmediatePropagation(); 401 | UpdateViewTransform(ViewTransform.position + Vector3.Scale(e.mouseDelta, ViewTransform.scale)); 402 | m_DraggingView = false; 403 | } 404 | } 405 | 406 | private void OnDropEnter(DropEnterEvent e) 407 | { 408 | if (e.GetUserData() is IDropPayload dropPayload && 409 | typeof(BaseEdge).IsAssignableFrom(dropPayload.GetPayloadType())) 410 | { 411 | // Consume event 412 | e.StopImmediatePropagation(); 413 | } 414 | } 415 | 416 | private void OnDrop(DropEvent e) 417 | { 418 | if (e.GetUserData() is IDropPayload dropPayload && 419 | typeof(BaseEdge).IsAssignableFrom(dropPayload.GetPayloadType())) 420 | { 421 | // Consume event 422 | e.StopImmediatePropagation(); 423 | 424 | // Delete edges 425 | for (int i = dropPayload.GetPayload().Count - 1; i >= 0; i--) 426 | { 427 | // Grab the edge 428 | BaseEdge edge = (BaseEdge)dropPayload.GetPayload()[i]; 429 | 430 | // Delete real edge 431 | if (edge.IsRealEdge()) { ExecuteEdgeDelete(edge); } 432 | 433 | // Delete candidate edge 434 | else { RemoveElement(edge); } 435 | } 436 | } 437 | } 438 | 439 | private void OnDropExit(DropExitEvent e) 440 | { 441 | if (e.GetUserData() is IDropPayload dropPayload && 442 | typeof(BaseEdge).IsAssignableFrom(dropPayload.GetPayloadType())) 443 | { 444 | // Consume event 445 | e.StopImmediatePropagation(); 446 | } 447 | } 448 | 449 | private bool IsMarqueeDrag(DragAndDropEvent e) where T : DragAndDropEvent, new() 450 | { 451 | if ((MouseButton)e.button != MouseButton.LeftMouse) { return false; } 452 | if (e.modifiers.IsNone()) { return true; } 453 | if (e.modifiers.IsExclusiveShift()) { return true; } 454 | if (e.modifiers.IsExclusiveActionKey()) { return true; } 455 | return false; 456 | } 457 | 458 | private bool IsViewDrag(DragAndDropEvent e) where T : DragAndDropEvent, new() 459 | { 460 | if ((MouseButton)e.button != MouseButton.MiddleMouse) { return false; } 461 | if (!e.modifiers.IsNone()) { return false; } 462 | return true; 463 | } 464 | #endregion 465 | 466 | #region Keybinding 467 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 468 | private static bool IsUnmodified(EventModifiers modifiers) => modifiers == EventModifiers.None; 469 | 470 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 471 | private static bool IsCommand(EventModifiers modifiers) => (modifiers & EventModifiers.Command) != 0; 472 | 473 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 474 | private static bool IsShift(EventModifiers modifiers) => (modifiers & EventModifiers.Shift) != 0; 475 | 476 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 477 | private static bool IsFunction(EventModifiers modifiers) => (modifiers & EventModifiers.FunctionKey) != 0; 478 | 479 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 480 | private static bool IsCommandExclusive(EventModifiers modifiers) => modifiers == EventModifiers.Command; 481 | 482 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 483 | private static bool IsControlExclusive(EventModifiers modifiers) => modifiers == EventModifiers.Control; 484 | 485 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 486 | private static bool IsMac() 487 | => Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer; 488 | 489 | [EventInterest(typeof(KeyDownEvent))] 490 | protected override void ExecuteDefaultAction(EventBase baseEvent) 491 | { 492 | if (baseEvent is not KeyDownEvent evt) { return; } 493 | if (panel.GetCapturingElement(PointerId.mousePointerId) != null) { return; } 494 | 495 | // Check for CMD or CTRL 496 | switch (evt.keyCode) 497 | { 498 | case KeyCode.C: 499 | if (IsMac()) 500 | { 501 | if (IsCommandExclusive(evt.modifiers)) 502 | { 503 | ExecuteCopy(); 504 | evt.StopPropagation(); 505 | } 506 | } 507 | else 508 | { 509 | if (IsControlExclusive(evt.modifiers)) 510 | { 511 | ExecuteCopy(); 512 | evt.StopPropagation(); 513 | } 514 | } 515 | break; 516 | case KeyCode.V: 517 | if (IsMac()) 518 | { 519 | if (IsCommandExclusive(evt.modifiers)) 520 | { 521 | ExecutePaste(); 522 | evt.StopPropagation(); 523 | } 524 | } 525 | else 526 | { 527 | if (IsControlExclusive(evt.modifiers)) 528 | { 529 | ExecutePaste(); 530 | evt.StopPropagation(); 531 | } 532 | } 533 | break; 534 | case KeyCode.X: 535 | if (IsMac()) 536 | { 537 | if (IsCommandExclusive(evt.modifiers)) 538 | { 539 | ExecuteCut(); 540 | evt.StopPropagation(); 541 | } 542 | } 543 | else 544 | { 545 | if (IsControlExclusive(evt.modifiers)) 546 | { 547 | ExecuteCut(); 548 | evt.StopPropagation(); 549 | } 550 | } 551 | break; 552 | case KeyCode.D: 553 | if (IsMac()) 554 | { 555 | if (IsCommandExclusive(evt.modifiers)) 556 | { 557 | ExecuteDuplicate(); 558 | evt.StopPropagation(); 559 | } 560 | } 561 | else 562 | { 563 | if (IsControlExclusive(evt.modifiers)) 564 | { 565 | ExecuteDuplicate(); 566 | evt.StopPropagation(); 567 | } 568 | } 569 | break; 570 | case KeyCode.Z: 571 | if (IsMac()) 572 | { 573 | if (IsCommandExclusive(evt.modifiers)) 574 | { 575 | ExecuteUndo(); 576 | evt.StopPropagation(); 577 | } 578 | else if (IsShift(evt.modifiers) && IsCommand(evt.modifiers)) 579 | { 580 | ExecuteRedo(); 581 | evt.StopPropagation(); 582 | } 583 | } 584 | else 585 | { 586 | if (IsControlExclusive(evt.modifiers)) 587 | { 588 | ExecuteUndo(); 589 | evt.StopPropagation(); 590 | } 591 | } 592 | break; 593 | case KeyCode.Y: 594 | if (!IsMac() && IsControlExclusive(evt.modifiers)) 595 | { 596 | ExecuteRedo(); 597 | evt.StopPropagation(); 598 | } 599 | break; 600 | case KeyCode.Delete: 601 | if (IsMac()) 602 | { 603 | if (IsUnmodified(evt.modifiers)) 604 | { 605 | ExecuteDelete(); 606 | evt.StopPropagation(); 607 | } 608 | } 609 | else 610 | { 611 | if (IsUnmodified(evt.modifiers)) 612 | { 613 | ExecuteDelete(); 614 | evt.StopPropagation(); 615 | } 616 | } 617 | break; 618 | case KeyCode.Backspace: 619 | if (IsMac() && IsCommand(evt.modifiers) && IsFunction(evt.modifiers)) 620 | { 621 | ExecuteDelete(); 622 | evt.StopPropagation(); 623 | } 624 | break; 625 | case KeyCode.F: 626 | if (IsUnmodified(evt.modifiers)) 627 | { 628 | Frame(); 629 | evt.StopPropagation(); 630 | } 631 | break; 632 | } 633 | } 634 | #endregion 635 | 636 | #region Add / Remove Elements from Heirarchy 637 | public void AddElement(GraphElement graphElement) 638 | { 639 | graphElement.Graph = this; 640 | GetLayer(graphElement.Layer).Add(graphElement); 641 | } 642 | 643 | public void RemoveElement(GraphElement graphElement) 644 | { 645 | UntrackElementForPan(graphElement); // Stop panning if we were panning 646 | graphElement.RemoveFromHierarchy(); 647 | graphElement.Graph = null; 648 | } 649 | #endregion 650 | 651 | #region Ports 652 | public void ConnectPorts(BasePort input, BasePort output) { AddElement(input.ConnectTo(output)); } 653 | 654 | internal void IlluminateCompatiblePorts(BasePort port) 655 | { 656 | foreach (BasePort otherPort in ContentContainer.Ports) 657 | { 658 | otherPort.Highlight = port.CanConnectTo(otherPort); 659 | } 660 | } 661 | 662 | internal void IlluminateAllPorts() 663 | { 664 | foreach (BasePort otherPort in ContentContainer.Ports) { otherPort.Highlight = true; } 665 | } 666 | #endregion 667 | 668 | #region Framing 669 | protected void Frame() 670 | { 671 | // Construct rect for selected and unselected elements 672 | Rect rectToFitSelected = ContentContainer.layout; 673 | Rect rectToFitUnselected = rectToFitSelected; 674 | bool reachedFirstSelected = false; 675 | bool reachedFirstUnselected = false; 676 | foreach (GraphElement ge in ContentContainer.ElementsAll) 677 | { 678 | // TODO: edge control is the VisualElement with actual dimensions 679 | // VisualElement ve = ge is BaseEdge edge ? edge.EdgeControl : ge; 680 | if (ge.Selected) 681 | { 682 | if (!reachedFirstSelected) 683 | { 684 | rectToFitSelected = ge.ChangeCoordinatesTo(ContentContainer, ge.Rect()); 685 | reachedFirstSelected = true; 686 | } 687 | else 688 | { 689 | rectToFitSelected = RectUtils.Encompass(rectToFitSelected, 690 | ge.ChangeCoordinatesTo(ContentContainer, ge.Rect())); 691 | } 692 | } 693 | else if (!reachedFirstSelected) // Don't bother if we already have at least one selected item 694 | { 695 | if (!reachedFirstUnselected) 696 | { 697 | rectToFitUnselected = ge.ChangeCoordinatesTo(ContentContainer, ge.Rect()); 698 | reachedFirstUnselected = true; 699 | } 700 | else 701 | { 702 | rectToFitUnselected = RectUtils.Encompass(rectToFitUnselected, 703 | ge.ChangeCoordinatesTo(ContentContainer, ge.Rect())); 704 | } 705 | } 706 | } 707 | 708 | // Use selection only if possible, otherwise unselected. Failing both, use original content container rect 709 | Vector3 frameScaling; 710 | Vector3 frameTranslation; 711 | if (reachedFirstSelected) 712 | { 713 | CalculateFrameTransform(rectToFitSelected, layout, k_FrameBorder, out frameTranslation, 714 | out frameScaling); 715 | } 716 | else if (reachedFirstUnselected) 717 | { 718 | CalculateFrameTransform(rectToFitUnselected, layout, k_FrameBorder, out frameTranslation, 719 | out frameScaling); 720 | } 721 | else 722 | { 723 | // Note: rectToFitSelected will just be the container rect 724 | CalculateFrameTransform(rectToFitSelected, layout, k_FrameBorder, out frameTranslation, 725 | out frameScaling); 726 | } 727 | 728 | // Update transform 729 | Matrix4x4.TRS(frameTranslation, Quaternion.identity, frameScaling); 730 | UpdateViewTransform(frameTranslation, frameScaling); 731 | } 732 | 733 | private float ZoomRequiredToFrameRect(Rect rectToFit, Rect clientRect, int border) 734 | { 735 | // bring slightly smaller screen rect into GUI space 736 | Rect screenRect = new() 737 | { 738 | xMin = border, 739 | xMax = clientRect.width - border, 740 | yMin = border, 741 | yMax = clientRect.height - border 742 | }; 743 | Rect identity = GUIUtility.ScreenToGUIRect(screenRect); 744 | return Math.Min(identity.width / rectToFit.width, identity.height / rectToFit.height); 745 | } 746 | 747 | public void CalculateFrameTransform(Rect rectToFit, Rect clientRect, int border, out Vector3 frameTranslation, 748 | out Vector3 frameScaling) 749 | { 750 | // measure zoom level necessary to fit the canvas rect into the screen rect 751 | float zoomLevel = ZoomRequiredToFrameRect(rectToFit, clientRect, border); 752 | 753 | // clamp 754 | zoomLevel = Mathf.Clamp(zoomLevel, MinScale, MaxScale); 755 | 756 | Matrix4x4 transformMatrix = 757 | Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new(zoomLevel, zoomLevel, 1.0f)); 758 | 759 | Vector2 edge = new(clientRect.width, clientRect.height); 760 | Vector2 origin = new(0, 0); 761 | 762 | Rect r = new() 763 | { 764 | min = origin, 765 | max = edge 766 | }; 767 | 768 | Vector3 parentScale = new(transformMatrix.GetColumn(0).magnitude, 769 | transformMatrix.GetColumn(1).magnitude, 770 | transformMatrix.GetColumn(2).magnitude); 771 | Vector2 offset = r.center - rectToFit.center * parentScale.x; 772 | 773 | // Update output values before leaving 774 | frameTranslation = new(offset.x, offset.y, 0.0f); 775 | frameScaling = parentScale; 776 | } 777 | #endregion 778 | 779 | #region Commands and Callbacks 780 | protected internal abstract void ExecuteCopy(); 781 | protected internal abstract void ExecuteCut(); 782 | protected internal abstract void ExecutePaste(); 783 | protected internal abstract void ExecuteDuplicate(); 784 | protected internal abstract void ExecuteDelete(); 785 | protected internal abstract void ExecuteUndo(); 786 | protected internal abstract void ExecuteRedo(); 787 | protected internal abstract void ExecuteEdgeCreate(BaseEdge edge); 788 | protected internal abstract void ExecuteEdgeDelete(BaseEdge edge); 789 | protected internal abstract void OnViewportChanged(); 790 | #endregion 791 | } 792 | } -------------------------------------------------------------------------------- /Elements/GridBackground.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using System; 6 | using UnityEngine; 7 | using UnityEngine.UIElements; 8 | 9 | namespace GraphViewPlayer 10 | { 11 | public class GridBackground : VisualElement 12 | { 13 | private static readonly CustomStyleProperty s_SpacingProperty = new("--grid-spacing"); 14 | private static readonly CustomStyleProperty s_ThickLinesProperty = new("--grid-thick-lines"); 15 | private static readonly CustomStyleProperty s_LineColorProperty = new("--grid-line-color"); 16 | private static readonly CustomStyleProperty s_ThickLineColorProperty = new("--grid-thick-line-color"); 17 | private static readonly CustomStyleProperty s_GridBackgroundColorProperty = 18 | new("--grid-background-color"); 19 | 20 | private static readonly float s_DefaultSpacing = 50f; 21 | private static readonly int s_DefaultThickLines = 10; 22 | private static readonly Color s_DefaultLineColor = new(0f, 0f, 0f, 0.18f); 23 | private static readonly Color s_DefaultThickLineColor = new(0f, 0f, 0f, 0.38f); 24 | private static readonly Color s_DefaultGridBackgroundColor = new(0.17f, 0.17f, 0.17f, 1.0f); 25 | 26 | private VisualElement m_Container; 27 | private GraphView m_GraphView; 28 | private Color m_LineColor = s_DefaultLineColor; 29 | 30 | private float m_Spacing = s_DefaultSpacing; 31 | private Color m_ThickLineColor = s_DefaultThickLineColor; 32 | private int m_ThickLines = s_DefaultThickLines; 33 | 34 | public GridBackground() 35 | { 36 | AddToClassList("grid-background"); 37 | pickingMode = PickingMode.Ignore; 38 | style.backgroundColor = s_DefaultGridBackgroundColor; 39 | this.StretchToParentSize(); 40 | generateVisualContent = OnGenerateVisualContent; 41 | RegisterCallback(OnAttachEvent); 42 | RegisterCallback(DetachFromPanelEvent); 43 | RegisterCallback(OnCustomStyleResolved); 44 | } 45 | 46 | private Vector3 Clip(Rect clipRect, Vector3 toClip) 47 | { 48 | if (toClip.x < clipRect.xMin) { toClip.x = clipRect.xMin; } 49 | if (toClip.x > clipRect.xMax) { toClip.x = clipRect.xMax; } 50 | if (toClip.y < clipRect.yMin) { toClip.y = clipRect.yMin; } 51 | if (toClip.y > clipRect.yMax) { toClip.y = clipRect.yMax; } 52 | return toClip; 53 | } 54 | 55 | private void OnCustomStyleResolved(CustomStyleResolvedEvent e) 56 | { 57 | ICustomStyle newStyle = e.customStyle; 58 | if (newStyle.TryGetValue(s_SpacingProperty, out float spacing)) { m_Spacing = spacing; } 59 | if (newStyle.TryGetValue(s_ThickLinesProperty, out int thickLine)) { m_ThickLines = thickLine; } 60 | if (newStyle.TryGetValue(s_ThickLineColorProperty, out Color thickLineColor)) 61 | { 62 | m_ThickLineColor = thickLineColor; 63 | } 64 | if (newStyle.TryGetValue(s_LineColorProperty, out Color lineColor)) { m_LineColor = lineColor; } 65 | if (newStyle.TryGetValue(s_GridBackgroundColorProperty, out Color gridColor)) 66 | { 67 | style.backgroundColor = gridColor; 68 | } 69 | } 70 | 71 | private void OnAttachEvent(AttachToPanelEvent evt) 72 | { 73 | // Parent must be GraphView 74 | VisualElement target = parent; 75 | m_GraphView = target as GraphView; 76 | if (m_GraphView == null) 77 | { 78 | throw new InvalidOperationException("GridBackground can only be added to a GraphView"); 79 | } 80 | m_Container = m_GraphView.ContentContainer; 81 | 82 | // Listen for Zoom/Pan Changes 83 | m_GraphView.OnViewTransformChanged += RequestRepaint; 84 | } 85 | 86 | private void DetachFromPanelEvent(DetachFromPanelEvent evt) 87 | { 88 | // Stop Listening for Zoom/Pan Changes 89 | m_GraphView.OnViewTransformChanged += RequestRepaint; 90 | } 91 | 92 | private void RequestRepaint(GraphView graphView) => MarkDirtyRepaint(); 93 | 94 | private void OnGenerateVisualContent(MeshGenerationContext ctx) 95 | { 96 | Rect clientRect = m_GraphView.layout; 97 | Painter2D painter = ctx.painter2D; 98 | 99 | // Since we're always stretch to parent size, we will use (0,0) as (x,y) coordinates 100 | clientRect.x = 0; 101 | clientRect.y = 0; 102 | 103 | Vector3 containerScale = new(m_Container.transform.matrix.GetColumn(0).magnitude, 104 | m_Container.transform.matrix.GetColumn(1).magnitude, 105 | m_Container.transform.matrix.GetColumn(2).magnitude); 106 | Vector4 containerTranslation = m_Container.transform.matrix.GetColumn(3); 107 | Rect containerPosition = m_Container.layout; 108 | float xSpacingThin = m_Spacing * containerScale.x; 109 | float xSpacingThick = xSpacingThin * m_ThickLines; 110 | float ySpacingThin = m_Spacing * containerScale.y; 111 | float ySpacingThick = ySpacingThin * m_ThickLines; 112 | 113 | // vertical lines 114 | Vector3 from = new(clientRect.x, clientRect.y, 0.0f); 115 | Vector3 to = new(clientRect.x, clientRect.height, 0.0f); 116 | 117 | Matrix4x4 tx = Matrix4x4.TRS(containerTranslation, Quaternion.identity, Vector3.one); 118 | 119 | from = tx.MultiplyPoint(from); 120 | to = tx.MultiplyPoint(to); 121 | 122 | from.x += containerPosition.x * containerScale.x; 123 | from.y += containerPosition.y * containerScale.y; 124 | to.x += containerPosition.x * containerScale.x; 125 | to.y += containerPosition.y * containerScale.y; 126 | 127 | float thickGridLineX = from.x; 128 | float thickGridLineY = from.y; 129 | 130 | // Update from/to to start at beginning of clientRect 131 | from.x = from.x % xSpacingThin - xSpacingThin; 132 | to.x = from.x; 133 | from.y = clientRect.y; 134 | to.y = clientRect.y + clientRect.height; 135 | while (from.x < clientRect.width) 136 | { 137 | from.x += xSpacingThin; 138 | to.x += xSpacingThin; 139 | 140 | painter.strokeColor = m_LineColor; 141 | painter.lineWidth = 1.0f; 142 | painter.BeginPath(); 143 | painter.MoveTo(Clip(clientRect, from)); 144 | painter.LineTo(Clip(clientRect, to)); 145 | painter.Stroke(); 146 | } 147 | 148 | float thickLineSpacing = m_Spacing * m_ThickLines; 149 | from.x = to.x = thickGridLineX % xSpacingThick - xSpacingThick; 150 | while (from.x < clientRect.width + thickLineSpacing) 151 | { 152 | painter.strokeColor = m_ThickLineColor; 153 | painter.lineWidth = 1.0f; 154 | painter.BeginPath(); 155 | painter.MoveTo(Clip(clientRect, from)); 156 | painter.LineTo(Clip(clientRect, to)); 157 | painter.Stroke(); 158 | 159 | from.x += xSpacingThick; 160 | to.x += xSpacingThick; 161 | } 162 | 163 | // horizontal lines 164 | from = new(clientRect.x, clientRect.y, 0.0f); 165 | to = new(clientRect.x + clientRect.width, clientRect.y, 0.0f); 166 | 167 | from.x += containerPosition.x * containerScale.x; 168 | from.y += containerPosition.y * containerScale.y; 169 | to.x += containerPosition.x * containerScale.x; 170 | to.y += containerPosition.y * containerScale.y; 171 | 172 | from = tx.MultiplyPoint(from); 173 | to = tx.MultiplyPoint(to); 174 | 175 | from.y = to.y = from.y % ySpacingThin - ySpacingThin; 176 | from.x = clientRect.x; 177 | to.x = clientRect.width; 178 | while (from.y < clientRect.height) 179 | { 180 | from.y += ySpacingThin; 181 | to.y += ySpacingThin; 182 | 183 | painter.strokeColor = m_LineColor; 184 | painter.lineWidth = 1.0f; 185 | painter.BeginPath(); 186 | painter.MoveTo(Clip(clientRect, from)); 187 | painter.LineTo(Clip(clientRect, to)); 188 | painter.Stroke(); 189 | } 190 | 191 | thickLineSpacing = m_Spacing * m_ThickLines; 192 | from.y = to.y = thickGridLineY % ySpacingThick - ySpacingThick; 193 | while (from.y < clientRect.height + thickLineSpacing) 194 | { 195 | painter.strokeColor = m_ThickLineColor; 196 | painter.lineWidth = 1.0f; 197 | painter.BeginPath(); 198 | painter.MoveTo(Clip(clientRect, from)); 199 | painter.LineTo(Clip(clientRect, to)); 200 | painter.Stroke(); 201 | 202 | from.y += ySpacingThick; 203 | to.y += ySpacingThick; 204 | } 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /Elements/Marquee.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.UIElements; 4 | 5 | namespace GraphViewPlayer 6 | { 7 | internal class Marquee : VisualElement 8 | { 9 | private Vector2 m_End; 10 | private Vector2 m_Start; 11 | 12 | internal Marquee() 13 | { 14 | AddToClassList("marquee"); 15 | pickingMode = PickingMode.Ignore; 16 | generateVisualContent = OnGenerateVisualContent; 17 | } 18 | 19 | internal Vector2 Start 20 | { 21 | get => m_Start; 22 | set 23 | { 24 | m_Start = value; 25 | MarkDirtyRepaint(); 26 | } 27 | } 28 | 29 | internal Vector2 End 30 | { 31 | get => m_End; 32 | set 33 | { 34 | m_End = value; 35 | MarkDirtyRepaint(); 36 | } 37 | } 38 | 39 | internal RectangleCoordinates Coordinates 40 | { 41 | get => new() { start = m_Start, end = m_End }; 42 | set 43 | { 44 | m_Start = value.start; 45 | m_End = value.end; 46 | MarkDirtyRepaint(); 47 | } 48 | } 49 | 50 | internal Rect SelectionRect => 51 | new() 52 | { 53 | min = new(Math.Min(m_Start.x, m_End.x), Math.Min(m_Start.y, m_End.y)), 54 | max = new(Math.Max(m_Start.x, m_End.x), Math.Max(m_Start.y, m_End.y)) 55 | }; 56 | 57 | private void OnGenerateVisualContent(MeshGenerationContext ctx) 58 | { 59 | Rect selectionRect = SelectionRect; 60 | Painter2D painter = ctx.painter2D; 61 | painter.lineWidth = 1.0f; 62 | painter.strokeColor = Color.white; 63 | painter.fillColor = Color.gray; 64 | painter.BeginPath(); 65 | painter.MoveTo(new(selectionRect.xMin, selectionRect.yMin)); 66 | painter.LineTo(new(selectionRect.xMax, selectionRect.yMin)); 67 | painter.LineTo(new(selectionRect.xMax, selectionRect.yMax)); 68 | painter.LineTo(new(selectionRect.xMin, selectionRect.yMax)); 69 | painter.LineTo(new(selectionRect.xMin, selectionRect.yMin)); 70 | painter.Stroke(); 71 | painter.Fill(); 72 | } 73 | 74 | internal struct RectangleCoordinates 75 | { 76 | internal Vector2 start; 77 | internal Vector2 end; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Enums/Capabilities.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using System; 6 | 7 | namespace GraphViewPlayer 8 | { 9 | [Flags] 10 | public enum Capabilities 11 | { 12 | Selectable = 1 << 0, 13 | Collapsible = 1 << 1, 14 | Resizable = 1 << 2, 15 | Movable = 1 << 3, 16 | Deletable = 1 << 4, 17 | Droppable = 1 << 5, 18 | Ascendable = 1 << 6, 19 | Renamable = 1 << 7, 20 | Copiable = 1 << 8, 21 | Snappable = 1 << 9, 22 | Groupable = 1 << 10, 23 | Stackable = 1 << 11 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Enums/Direction.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | namespace GraphViewPlayer 6 | { 7 | public enum Direction 8 | { 9 | Input = 0, 10 | Output = 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Enums/Orientation.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | namespace GraphViewPlayer 6 | { 7 | public enum Orientation 8 | { 9 | Horizontal, 10 | Vertical 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Enums/PortCapacity.cs: -------------------------------------------------------------------------------- 1 | namespace GraphViewPlayer 2 | { 3 | public enum PortCapacity 4 | { 5 | Single, 6 | Multi, 7 | } 8 | } -------------------------------------------------------------------------------- /GraphViewPlayer.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GraphViewPlayer" 3 | } 4 | -------------------------------------------------------------------------------- /Interfaces/IPositionable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.UIElements; 4 | 5 | namespace GraphViewPlayer 6 | { 7 | public interface IPositionable 8 | { 9 | public event Action OnPositionChange; 10 | public Vector2 GetGlobalCenter(); 11 | public Vector2 GetCenter(); 12 | public Vector2 GetPosition(); 13 | public void SetPosition(Vector2 position); 14 | public void ApplyDeltaToPosition(Vector2 delta); 15 | } 16 | 17 | public struct PositionData 18 | { 19 | public Vector2 position; 20 | public VisualElement element; 21 | } 22 | } -------------------------------------------------------------------------------- /Interfaces/ISelectable.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace GraphViewPlayer 4 | { 5 | public interface ISelectable 6 | { 7 | ISelector Selector { get; } 8 | bool Selected { get; set; } 9 | bool Overlaps(Rect rectangle); 10 | bool ContainsPoint(Vector2 localPoint); 11 | } 12 | } -------------------------------------------------------------------------------- /Interfaces/ISelector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace GraphViewPlayer 5 | { 6 | public interface ISelector 7 | { 8 | void SelectAll(); 9 | void ClearSelection(); 10 | void CollectAll(List toPopulate); 11 | void CollectSelected(List toPopulate); 12 | void CollectUnselected(List toPopulate); 13 | void ForEachAll(Action action); 14 | void ForEachSelected(Action action); 15 | void ForEachUnselected(Action action); 16 | } 17 | } -------------------------------------------------------------------------------- /Manipulators/DragAndDropManipulator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.UIElements; 5 | 6 | namespace GraphViewPlayer 7 | { 8 | public class DragAndDropManipulator : Manipulator 9 | { 10 | private readonly List m_PickList; 11 | private bool m_BreachedDragThreshold; 12 | private VisualElement m_Dragged; 13 | 14 | private float m_DragThreshold; 15 | private bool m_MouseDown; 16 | private Vector2 m_MouseOrigin; 17 | private VisualElement m_PreviousDropTarget; 18 | private Vector2 m_PreviousMousePosition; 19 | private object m_UserData; 20 | 21 | #region Constructor 22 | public DragAndDropManipulator() => m_PickList = new(); 23 | #endregion 24 | 25 | protected override void RegisterCallbacksOnTarget() 26 | { 27 | target.RegisterCallback(OnMouseDown); 28 | target.RegisterCallback(OnMouseMove); 29 | target.RegisterCallback(OnMouseUp); 30 | target.RegisterCallback(OnMouseCaptureOutEvent); 31 | target.RegisterCallback(OnKeyDown); 32 | } 33 | 34 | protected override void UnregisterCallbacksFromTarget() 35 | { 36 | target.UnregisterCallback(OnMouseDown); 37 | target.UnregisterCallback(OnMouseMove); 38 | target.UnregisterCallback(OnMouseUp); 39 | target.UnregisterCallback(OnMouseCaptureOutEvent); 40 | target.UnregisterCallback(OnKeyDown); 41 | } 42 | 43 | #region Event Handlers 44 | private void OnMouseDown(MouseDownEvent e) 45 | { 46 | // Don't tolerate clicking other buttons while dragging 47 | if (m_MouseDown) 48 | { 49 | CancelDrag(e); 50 | return; 51 | } 52 | 53 | // Create event 54 | m_MouseDown = true; 55 | m_MouseOrigin = e.mousePosition; 56 | using (DragOfferEvent dragOfferEvent = DragOfferEvent.GetPooled(e)) 57 | { 58 | // Set parent 59 | dragOfferEvent.ParentManipulator = this; 60 | 61 | // Set correct drag delta 62 | dragOfferEvent.SetMouseDelta(Vector2.zero); 63 | 64 | // Send event 65 | target.SendEvent(dragOfferEvent); 66 | } 67 | 68 | // Record mouse position 69 | m_PreviousMousePosition = e.mousePosition; 70 | } 71 | 72 | internal void OnDragOfferComplete(DragOfferEvent dragOfferEvent) 73 | { 74 | // Check for cancel request or nobody accepting the event 75 | if (dragOfferEvent.IsCancelled() || m_Dragged == null || m_Dragged.parent == null) { Reset(); } 76 | else 77 | { 78 | // Capture any threshold requests 79 | m_DragThreshold = dragOfferEvent.GetDragThreshold(); 80 | 81 | // Capturing ensures we retain the mouse events even if the mouse leaves the target. 82 | target.CaptureMouse(); 83 | } 84 | } 85 | 86 | private void OnMouseMove(MouseMoveEvent e) 87 | { 88 | // If the mouse isn't down, bail 89 | if (!m_MouseDown) { return; } 90 | 91 | // Check if dragged is still in the hierarchy 92 | if (m_Dragged.parent == null) 93 | { 94 | CancelDrag(e); 95 | return; 96 | } 97 | 98 | // Check if we're starting a drag 99 | if (!m_BreachedDragThreshold) 100 | { 101 | // Have we breached the drag threshold? 102 | Vector2 mouseOriginDiff = m_MouseOrigin - e.mousePosition; 103 | if (Mathf.Abs(mouseOriginDiff.magnitude) < m_DragThreshold) 104 | { 105 | // Not time yet to start dragging 106 | return; 107 | } 108 | 109 | // Drag begin 110 | using (DragBeginEvent dragBeginEvent = DragBeginEvent.GetPooled(e)) 111 | { 112 | // Set parent 113 | dragBeginEvent.ParentManipulator = this; 114 | 115 | // Set target 116 | dragBeginEvent.target = m_Dragged; 117 | 118 | // Send event 119 | target.SendEvent(dragBeginEvent); 120 | } 121 | 122 | // We are starting a drag! 123 | m_BreachedDragThreshold = true; 124 | 125 | // Sending drag event after drag begin so we can check for cancellation 126 | return; 127 | } 128 | 129 | // Send drag event 130 | SendDragEvent(e); 131 | } 132 | 133 | internal void OnDragBeginComplete(DragBeginEvent dragBeginEvent) 134 | { 135 | // Check for cancel request 136 | if (dragBeginEvent.IsCancelled() || m_Dragged.parent == null) 137 | { 138 | CancelDrag(dragBeginEvent); 139 | return; 140 | } 141 | 142 | // No cancellation, send drag event 143 | SendDragEvent(dragBeginEvent); 144 | } 145 | 146 | private void SendDragEvent(MouseEventBase e) where T : MouseEventBase, new() 147 | { 148 | // We breached the threshold, time to drag 149 | using (DragEvent dragEvent = DragEvent.GetPooled(e)) 150 | { 151 | // Set parent 152 | dragEvent.ParentManipulator = this; 153 | 154 | // Set target 155 | dragEvent.target = m_Dragged; 156 | 157 | // Set correct drag delta 158 | dragEvent.SetMouseDelta(e.mousePosition - m_PreviousMousePosition); 159 | 160 | // Send event 161 | target.SendEvent(dragEvent); 162 | } 163 | 164 | // Record mouse position 165 | m_PreviousMousePosition = e.mousePosition; 166 | } 167 | 168 | internal void OnDragComplete(DragEvent dragEvent) 169 | { 170 | // Check for cancel request 171 | if (dragEvent.IsCancelled() || m_Dragged.parent == null) 172 | { 173 | CancelDrag(dragEvent); 174 | return; 175 | } 176 | 177 | // Look for droppable 178 | VisualElement pick = PickFirstExcluding(dragEvent.mousePosition, m_Dragged); 179 | if (pick == m_PreviousDropTarget) { return; } 180 | 181 | // Check if we need to fire an exit event 182 | AttemptDropExit(dragEvent); 183 | 184 | // Record new pick 185 | m_PreviousDropTarget = pick; 186 | if (pick == null) { return; } 187 | 188 | // Send drop enter event 189 | using (DropEnterEvent dropEnterEvent = DropEnterEvent.GetPooled(dragEvent)) 190 | { 191 | // Set parent 192 | dropEnterEvent.ParentManipulator = this; 193 | 194 | // Set target 195 | dropEnterEvent.target = pick; 196 | 197 | // Drop enter 198 | target.SendEvent(dropEnterEvent); 199 | } 200 | } 201 | 202 | internal void OnDropEnterComplete(DropEnterEvent dropEnterEvent) { } 203 | 204 | private void OnMouseUp(MouseUpEvent e) 205 | { 206 | // If the mouse isn't down or we're not dragging 207 | if (!m_MouseDown) { return; } 208 | 209 | // Make sure the dragged element still exists 210 | if (m_Dragged.parent == null) 211 | { 212 | CancelDrag(e); 213 | return; 214 | } 215 | 216 | // Send drop event 217 | using (DropEvent dropEvent = DropEvent.GetPooled(e)) 218 | { 219 | // Set parent 220 | dropEvent.ParentManipulator = this; 221 | 222 | // Check if we skipped the drop 223 | if (m_PreviousDropTarget == null) 224 | { 225 | OnDropComplete(dropEvent); 226 | return; 227 | } 228 | 229 | // Set target 230 | dropEvent.target = m_PreviousDropTarget; 231 | 232 | // Send drop event 233 | target.SendEvent(dropEvent); 234 | } 235 | } 236 | 237 | internal void OnDropComplete(DropEvent dropEvent) 238 | { 239 | // Check if cancelled 240 | if (dropEvent.IsCancelled() || m_Dragged.parent == null) 241 | { 242 | CancelDrag(dropEvent); 243 | return; 244 | } 245 | 246 | // Complete drag 247 | using (DragEndEvent dragEndEvent = DragEndEvent.GetPooled(dropEvent)) 248 | { 249 | // Set parent 250 | dragEndEvent.ParentManipulator = this; 251 | 252 | // Set target 253 | dragEndEvent.target = m_Dragged; 254 | 255 | // Set delta to a value that would reset the drag 256 | dragEndEvent.DeltaToDragOrigin = m_MouseOrigin - dragEndEvent.mousePosition; 257 | 258 | // Send event 259 | target.SendEvent(dragEndEvent); 260 | } 261 | } 262 | 263 | internal void OnDragEndComplete(DragEndEvent dragEndEvent) 264 | { 265 | // Reset everything 266 | Reset(); 267 | } 268 | 269 | private void CancelDrag(EventBase e) 270 | { 271 | if (m_BreachedDragThreshold) 272 | { 273 | using (DragCancelEvent dragCancelEvent = DragCancelEvent.GetPooled()) 274 | { 275 | // Set parent 276 | dragCancelEvent.ParentManipulator = this; 277 | 278 | // Set target 279 | dragCancelEvent.target = m_Dragged; 280 | 281 | // Set mouse delta to a value that would reset the drag 282 | dragCancelEvent.DeltaToDragOrigin = m_MouseOrigin - m_PreviousMousePosition; 283 | 284 | // Send cancel event 285 | target.SendEvent(dragCancelEvent); 286 | 287 | // Check for drag exit 288 | AttemptDropExit(dragCancelEvent); 289 | } 290 | } 291 | else { Reset(); } 292 | } 293 | 294 | internal void OnDragCancelComplete(DragCancelEvent dragCancelEvent) { Reset(); } 295 | 296 | private void AttemptDropExit(MouseEventBase e) where T : MouseEventBase, new() 297 | { 298 | if (m_PreviousDropTarget != null) 299 | { 300 | using (DropExitEvent dropExitEvent = DropExitEvent.GetPooled()) 301 | { 302 | // Set parent 303 | dropExitEvent.ParentManipulator = this; 304 | 305 | // Set target 306 | dropExitEvent.target = m_PreviousDropTarget; 307 | 308 | // Send event 309 | target.SendEvent(dropExitEvent); 310 | } 311 | } 312 | } 313 | 314 | internal void OnDropExitComplete(DropExitEvent dropExitEvent) { } 315 | 316 | private void OnMouseCaptureOutEvent(MouseCaptureOutEvent e) 317 | { 318 | if (m_MouseDown) { CancelDrag(e); } 319 | } 320 | 321 | private void OnKeyDown(KeyDownEvent e) 322 | { 323 | // Only listen to ESC while dragging 324 | if (e.keyCode != KeyCode.Escape || !m_MouseDown) { return; } 325 | 326 | // Notify the draggable 327 | CancelDrag(e); 328 | } 329 | 330 | internal void SetUserData(object userData) => m_UserData = userData; 331 | internal object GetUserData() => m_UserData; 332 | internal void SetDraggedElement(VisualElement element) => m_Dragged = element; 333 | internal VisualElement GetDraggedElement() => m_Dragged; 334 | #endregion 335 | 336 | #region Helpers 337 | private VisualElement PickFirstExcluding(Vector2 position, params VisualElement[] exclude) 338 | { 339 | target.panel.PickAll(position, m_PickList); 340 | if (m_PickList.Count == 0) { return null; } 341 | VisualElement toFind = null; 342 | for (int i = 0; i < m_PickList.Count; i++) 343 | { 344 | VisualElement visualElement = m_PickList[i]; 345 | if (exclude.Length > 0 && Array.IndexOf(exclude, visualElement) != -1) { continue; } 346 | toFind = visualElement; 347 | break; 348 | } 349 | m_PickList.Clear(); 350 | return toFind; 351 | } 352 | 353 | private void Reset() 354 | { 355 | m_Dragged = null; 356 | m_UserData = null; 357 | m_MouseDown = false; 358 | m_MouseOrigin = Vector2.zero; 359 | m_DragThreshold = 0; 360 | m_PreviousDropTarget = null; 361 | m_BreachedDragThreshold = false; 362 | m_PreviousMousePosition = Vector2.zero; 363 | target.ReleaseMouse(); 364 | } 365 | #endregion 366 | } 367 | 368 | #region Events 369 | public abstract class DragAndDropEvent : MouseEventBase where T : DragAndDropEvent, new() 370 | { 371 | protected bool m_Cancelled; 372 | protected Vector2 m_DeltaToOrigin; 373 | protected float m_DragThreshold; 374 | internal DragAndDropManipulator ParentManipulator { get; set; } 375 | 376 | public Vector2 DeltaToDragOrigin 377 | { 378 | get => m_DeltaToOrigin; 379 | internal set => m_DeltaToOrigin = value; 380 | } 381 | 382 | internal float GetDragThreshold() => m_DragThreshold; 383 | internal void SetMouseDelta(Vector2 delta) => mouseDelta = delta; 384 | public VisualElement GetDraggedElement() => ParentManipulator.GetDraggedElement(); 385 | public bool IsCancelled() => m_Cancelled; 386 | public void CancelDrag() => m_Cancelled = true; 387 | public object GetUserData() => ParentManipulator.GetUserData(); 388 | 389 | protected override void Init() 390 | { 391 | base.Init(); 392 | m_DeltaToOrigin = Vector2.zero; 393 | m_DragThreshold = 0; 394 | m_Cancelled = false; 395 | } 396 | } 397 | 398 | public abstract class DragEventBase : DragAndDropEvent where T : DragEventBase, new() 399 | { 400 | public void SetUserData(object data) => ParentManipulator.SetUserData(data); 401 | } 402 | 403 | public class DragOfferEvent : DragEventBase 404 | { 405 | public static DragOfferEvent GetPooled(DragOfferEvent e) 406 | { 407 | DragOfferEvent newEvent = MouseEventBase.GetPooled(e); 408 | if (newEvent != null) 409 | { 410 | newEvent.ParentManipulator = e.ParentManipulator; 411 | newEvent.m_DeltaToOrigin = e.m_DeltaToOrigin; 412 | newEvent.m_DragThreshold = e.m_DragThreshold; 413 | newEvent.m_Cancelled = e.m_Cancelled; 414 | } 415 | return newEvent; 416 | } 417 | 418 | public void SetDragThreshold(float threshold) => m_DragThreshold = threshold; 419 | 420 | public void AcceptDrag(VisualElement draggedElement) { ParentManipulator.SetDraggedElement(draggedElement); } 421 | 422 | protected override void PostDispatch(IPanel panel) 423 | { 424 | base.PostDispatch(panel); 425 | ParentManipulator.OnDragOfferComplete(this); 426 | } 427 | } 428 | 429 | public class DragBeginEvent : DragEventBase 430 | { 431 | protected override void PostDispatch(IPanel panel) 432 | { 433 | base.PostDispatch(panel); 434 | ParentManipulator.OnDragBeginComplete(this); 435 | } 436 | } 437 | 438 | public class DragEvent : DragEventBase 439 | { 440 | protected override void PostDispatch(IPanel panel) 441 | { 442 | base.PostDispatch(panel); 443 | ParentManipulator.OnDragComplete(this); 444 | } 445 | } 446 | 447 | public class DragEndEvent : DragEventBase 448 | { 449 | protected override void PostDispatch(IPanel panel) 450 | { 451 | base.PostDispatch(panel); 452 | ParentManipulator.OnDragEndComplete(this); 453 | } 454 | } 455 | 456 | public class DragCancelEvent : DragEventBase 457 | { 458 | protected override void PostDispatch(IPanel panel) 459 | { 460 | base.PostDispatch(panel); 461 | ParentManipulator.OnDragCancelComplete(this); 462 | } 463 | } 464 | 465 | public class DropEnterEvent : DragAndDropEvent 466 | { 467 | protected override void PostDispatch(IPanel panel) 468 | { 469 | base.PostDispatch(panel); 470 | ParentManipulator.OnDropEnterComplete(this); 471 | } 472 | } 473 | 474 | public class DropEvent : DragAndDropEvent 475 | { 476 | protected override void PostDispatch(IPanel panel) 477 | { 478 | base.PostDispatch(panel); 479 | ParentManipulator.OnDropComplete(this); 480 | } 481 | } 482 | 483 | public class DropExitEvent : DragAndDropEvent 484 | { 485 | protected override void PostDispatch(IPanel panel) 486 | { 487 | base.PostDispatch(panel); 488 | ParentManipulator.OnDropExitComplete(this); 489 | } 490 | } 491 | #endregion 492 | } -------------------------------------------------------------------------------- /Manipulators/SelectableManipulator.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using UnityEngine; 6 | using UnityEngine.UIElements; 7 | 8 | namespace GraphViewPlayer 9 | { 10 | public class SelectableManipulator : MouseManipulator 11 | { 12 | private ISelectable m_Selectable; 13 | 14 | public SelectableManipulator() 15 | { 16 | activators.Add(new() { button = MouseButton.LeftMouse }); 17 | activators.Add(new() { button = MouseButton.LeftMouse, modifiers = EventModifiers.Shift }); 18 | activators.Add(new() 19 | { 20 | button = MouseButton.LeftMouse, 21 | modifiers = PlatformUtils.IsMac ? EventModifiers.Command : EventModifiers.Control 22 | }); 23 | } 24 | 25 | protected override void RegisterCallbacksOnTarget() 26 | { 27 | m_Selectable = target as ISelectable; 28 | if (m_Selectable == null) { throw new("SelectableManipulator can only be added to ISelectable elements"); } 29 | target.RegisterCallback(OnMouseDown); 30 | } 31 | 32 | protected override void UnregisterCallbacksFromTarget() 33 | { 34 | target.UnregisterCallback(OnMouseDown); 35 | m_Selectable = null; 36 | } 37 | 38 | protected void OnMouseDown(MouseDownEvent e) 39 | { 40 | if (!CanStartManipulation(e)) { return; } 41 | 42 | // Do not stop the propagation since selection should be happening before other use cases 43 | bool additive = e.shiftKey; 44 | bool subtractive = e.actionKey; 45 | bool exclusive = !(additive ^ subtractive); 46 | if (exclusive) 47 | { 48 | if (!m_Selectable.Selected) 49 | { 50 | m_Selectable.Selector.ClearSelection(); 51 | m_Selectable.Selected = true; 52 | } 53 | } 54 | else { m_Selectable.Selected = !m_Selectable.Selected; } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Manipulators/ZoomManipulator.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using System; 6 | using UnityEngine; 7 | using UnityEngine.UIElements; 8 | 9 | namespace GraphViewPlayer 10 | { 11 | public class ZoomManipulator : Manipulator 12 | { 13 | public static readonly float DefaultReferenceScale = 1; 14 | public static readonly float DefaultMinScale = 0.25f; 15 | public static readonly float DefaultMaxScale = 1; 16 | public static readonly float DefaultScaleStep = 0.15f; 17 | 18 | /// 19 | /// Scale that should be computed when scroll wheel offset is at zero. 20 | /// 21 | public float ReferenceScale { get; set; } = DefaultReferenceScale; 22 | public float MinScale { get; set; } = DefaultMinScale; 23 | public float MaxScale { get; set; } = DefaultMaxScale; 24 | 25 | /// 26 | /// Relative scale change when zooming in/out (e.g. For 15%, use 0.15). 27 | /// 28 | /// 29 | /// Depending on the values of minScale, maxScale and scaleStep, it is not guaranteed that 30 | /// the first and last two scale steps will correspond exactly to the value specified in scaleStep. 31 | /// 32 | public float ScaleStep { get; set; } = DefaultScaleStep; 33 | 34 | protected override void RegisterCallbacksOnTarget() 35 | { 36 | GraphView graphView = target as GraphView; 37 | if (graphView == null) 38 | { 39 | throw new InvalidOperationException("Manipulator can only be added to a GraphView"); 40 | } 41 | 42 | target.RegisterCallback(OnWheel); 43 | } 44 | 45 | protected override void UnregisterCallbacksFromTarget() { target.UnregisterCallback(OnWheel); } 46 | 47 | // Compute the parameters of our exponential model: 48 | // z(w) = (1 + s) ^ (w + a) + b 49 | // Where 50 | // z: calculated zoom level 51 | // w: accumulated wheel deltas (1 unit = 1 mouse notch) 52 | // s: zoom step 53 | // 54 | // The factors a and b are calculated in order to satisfy the conditions: 55 | // z(0) = referenceZoom 56 | // z(1) = referenceZoom * (1 + zoomStep) 57 | private static float CalculateNewZoom(float currentZoom, float wheelDelta, float zoomStep, float referenceZoom, 58 | float minZoom, float maxZoom) 59 | { 60 | if (minZoom <= 0) 61 | { 62 | Debug.LogError($"The minimum zoom ({minZoom}) must be greater than zero."); 63 | return currentZoom; 64 | } 65 | if (referenceZoom < minZoom) 66 | { 67 | Debug.LogError( 68 | $"The reference zoom ({referenceZoom}) must be greater than or equal to the minimum zoom ({minZoom})."); 69 | return currentZoom; 70 | } 71 | if (referenceZoom > maxZoom) 72 | { 73 | Debug.LogError( 74 | $"The reference zoom ({referenceZoom}) must be less than or equal to the maximum zoom ({maxZoom})."); 75 | return currentZoom; 76 | } 77 | if (zoomStep < 0) 78 | { 79 | Debug.LogError($"The zoom step ({zoomStep}) must be greater than or equal to zero."); 80 | return currentZoom; 81 | } 82 | 83 | currentZoom = Mathf.Clamp(currentZoom, minZoom, maxZoom); 84 | 85 | if (Mathf.Approximately(wheelDelta, 0)) { return currentZoom; } 86 | 87 | // Calculate the factors of our model: 88 | double a = Math.Log(referenceZoom, 1 + zoomStep); 89 | double b = referenceZoom - Math.Pow(1 + zoomStep, a); 90 | 91 | // Convert zoom levels to scroll wheel values. 92 | double minWheel = Math.Log(minZoom - b, 1 + zoomStep) - a; 93 | double maxWheel = Math.Log(maxZoom - b, 1 + zoomStep) - a; 94 | double currentWheel = Math.Log(currentZoom - b, 1 + zoomStep) - a; 95 | 96 | // Except when the delta is zero, for each event, consider that the delta corresponds to a rotation by a 97 | // full notch. The scroll wheel abstraction system is buggy and incomplete: with a regular mouse, the 98 | // minimum wheel movement is 0.1 on OS X and 3 on Windows. We can't simply accumulate deltas like these, so 99 | // we accumulate integers only. This may be problematic with high resolution scroll wheels: many small 100 | // events will be fired. However, at this point, we have no way to differentiate a high resolution scroll 101 | // wheel delta from a non-accelerated scroll wheel delta of one notch on OS X. 102 | wheelDelta = Math.Sign(wheelDelta); 103 | currentWheel += wheelDelta; 104 | 105 | // Assimilate to the boundary when it is nearby. 106 | if (currentWheel > maxWheel - 0.5) { return maxZoom; } 107 | if (currentWheel < minWheel + 0.5) { return minZoom; } 108 | 109 | // Snap the wheel to the unit grid. 110 | currentWheel = Math.Round(currentWheel); 111 | 112 | // Do not assimilate again. Otherwise, points as far as 1.5 units away could be stuck to the boundary 113 | // because the wheel delta is either +1 or -1. 114 | 115 | // Calculate the corresponding zoom level. 116 | return (float)(Math.Pow(1 + zoomStep, currentWheel + a) + b); 117 | } 118 | 119 | private void OnWheel(WheelEvent evt) 120 | { 121 | GraphView graphView = target as GraphView; 122 | if (graphView == null) { return; } 123 | 124 | IPanel panel = (evt.target as VisualElement)?.panel; 125 | if (panel.GetCapturingElement(PointerId.mousePointerId) != null) { return; } 126 | 127 | Vector3 position = graphView.ViewTransform.position; 128 | Vector3 scale = graphView.ViewTransform.scale; 129 | 130 | // TODO: augment the data to have the position as well, so we don't have to read in data from the target. 131 | // 0-1 ranged center relative to size 132 | Vector2 zoomCenter = target.ChangeCoordinatesTo(graphView.ContentContainer, evt.localMousePosition); 133 | float x = zoomCenter.x + graphView.ContentContainer.layout.x; 134 | float y = zoomCenter.y + graphView.ContentContainer.layout.y; 135 | 136 | position += Vector3.Scale(new(x, y, 0), scale); 137 | 138 | // Apply the new zoom. 139 | float zoom = CalculateNewZoom(scale.y, -evt.delta.y, ScaleStep, ReferenceScale, MinScale, MaxScale); 140 | scale.x = zoom; 141 | scale.y = zoom; 142 | scale.z = 1; 143 | 144 | position -= Vector3.Scale(new(x, y, 0), scale); 145 | 146 | graphView.UpdateViewTransform(position, scale); 147 | 148 | evt.StopPropagation(); 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /Resources/GraphViewPlayer/GraphView.uss: -------------------------------------------------------------------------------- 1 | .grid-background { 2 | --grid-spacing: 50; 3 | --grid-thick-lines: 10; 4 | --grid-line-color: rgba(0, 0, 0, 0.18); 5 | --grid-thick-line-color: rgba(0, 0, 0, 0.38); 6 | --grid-background-color: rgba(45, 45, 45, 1); 7 | } 8 | 9 | .graph-view { 10 | overflow: hidden; 11 | flex-direction: column; 12 | font-size: 12px; 13 | } 14 | 15 | .backstop { 16 | position: absolute; 17 | width: 100%; 18 | height: 100%; 19 | } 20 | 21 | .content-view-container { 22 | position: absolute; 23 | } 24 | 25 | .graph-element { 26 | position: absolute; 27 | } 28 | 29 | .node { 30 | color: #b2b2b2; 31 | background-color: rgba(25, 25, 25, 1); 32 | position: absolute; 33 | border-radius: 6px; 34 | border-width: 1px 1px 1px 1px; 35 | border-color: black; 36 | min-width: 100px; 37 | } 38 | 39 | .node-selected { 40 | border-color: rgba(68, 192, 255, 1); 41 | } 42 | 43 | .node-title { 44 | flex-direction: row; 45 | justify-content: space-between; 46 | background-color: rgba(63, 63, 63, 0.804); 47 | height: 36px; 48 | border-top-left-radius: 6px; 49 | border-top-right-radius: 6px; 50 | } 51 | 52 | .node-title-label { 53 | color: rgba(193, 193, 193, 1); 54 | font-size: 12px; 55 | margin: 6px; 56 | padding-left: 2px; 57 | -unity-text-align: middle-left; 58 | } 59 | 60 | .node-io { 61 | flex-direction: row; 62 | flex-wrap: nowrap; 63 | border-top-width: 1px; 64 | border-bottom-width: 1px; 65 | border-color: black; 66 | background-color: rgba(46, 46, 46, 0.804); 67 | justify-content: space-between; 68 | } 69 | 70 | .node-io-input { 71 | flex-direction: column; 72 | background-color: rgba(60, 60, 60, 0.804); 73 | padding: 4px; 74 | } 75 | 76 | .node-io-output { 77 | flex-direction: column; 78 | background-color: rgba(46, 46, 46, 0.804); 79 | padding: 4px; 80 | } 81 | 82 | .node-extension { 83 | background-color: rgba(63, 63, 63, 0.804); 84 | flex-direction: column; 85 | padding-bottom: 12px; 86 | border-bottom-left-radius: 6px; 87 | border-bottom-right-radius: 6px; 88 | } 89 | 90 | .port { 91 | --port-color: rgba(240, 240, 255, 0.95); 92 | --disabled-port-color: rgba(70, 70, 70, 0.27); 93 | flex-grow: 1; 94 | } 95 | 96 | .port-input { 97 | flex-direction: row-reverse; 98 | margin-right: auto; 99 | } 100 | 101 | .port-output { 102 | flex-direction: row; 103 | margin-left: auto; 104 | } 105 | 106 | .port-label-container { 107 | margin: 0px; 108 | padding-top: 0px; 109 | padding-bottom: 0px; 110 | padding-left: 5px; 111 | padding-right: 5px; 112 | } 113 | 114 | .port-label { 115 | color: #b2b2b2; 116 | } 117 | 118 | .port-connector-box { 119 | margin-top: auto; 120 | margin-bottom: auto; 121 | margin-left: 5px; 122 | margin-right: 5px; 123 | border-width: 1px 1px 1px 1px; 124 | border-radius: 100% 100% 100% 100%; 125 | width: 10px; 126 | height: 10px; 127 | } 128 | 129 | .port-connector-cap { 130 | flex-grow: 1; 131 | border-radius: 100% 100% 100% 100%; 132 | margin: 1px 1px 1px 1px; 133 | } 134 | 135 | .edge { 136 | --edge-width: 2; 137 | --edge-width-selected: 2; 138 | --edge-color: rgba(146, 146, 146, 1); 139 | --edge-color-selected: rgba(240, 240, 240, 1); 140 | --layer: -10; 141 | position: absolute; 142 | } 143 | 144 | .edge-cap { 145 | border-radius: 100% 100% 100% 100%; /*12*/ 146 | width: 4px; 147 | height: 4px; 148 | background-color: rgba(146, 146, 146, 1); 149 | } 150 | 151 | .marquee { 152 | position: absolute; 153 | opacity: 0.25; 154 | } -------------------------------------------------------------------------------- /Utils/MouseUtils.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace GraphViewPlayer 4 | { 5 | public static class MouseUtils 6 | { 7 | public static bool IsNone(this EventModifiers modifiers) => modifiers == EventModifiers.None; 8 | public static bool IsShift(this EventModifiers modifiers) => (modifiers & EventModifiers.Shift) != 0; 9 | public static bool IsActionKey(this EventModifiers modifiers) 10 | => PlatformUtils.IsMac 11 | ? (modifiers & EventModifiers.Command) != 0 12 | : (modifiers & EventModifiers.Control) != 0; 13 | 14 | public static bool IsExclusiveShift(this EventModifiers modifiers) => modifiers == EventModifiers.Shift; 15 | public static bool IsExclusiveActionKey(this EventModifiers modifiers) 16 | => PlatformUtils.IsMac 17 | ? modifiers == EventModifiers.Command 18 | : modifiers == EventModifiers.Control; 19 | } 20 | } -------------------------------------------------------------------------------- /Utils/PlatformUtils.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace GraphViewPlayer 4 | { 5 | public static class PlatformUtils 6 | { 7 | public static readonly bool IsMac 8 | = Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer; 9 | } 10 | } -------------------------------------------------------------------------------- /Utils/RectUtils.cs: -------------------------------------------------------------------------------- 1 | // Unity C# reference source 2 | // Copyright (c) Unity Technologies. For terms of use, see 3 | // https://unity3d.com/legal/licenses/Unity_Reference_Only_License 4 | 5 | using System; 6 | using UnityEngine; 7 | using UnityEngine.UIElements; 8 | 9 | namespace GraphViewPlayer 10 | { 11 | public static class RectUtils 12 | { 13 | public static bool IntersectsSegment(Rect rect, Vector2 p1, Vector2 p2) 14 | { 15 | float minX = Mathf.Min(p1.x, p2.x); 16 | float maxX = Mathf.Max(p1.x, p2.x); 17 | 18 | if (maxX > rect.xMax) { maxX = rect.xMax; } 19 | 20 | if (minX < rect.xMin) { minX = rect.xMin; } 21 | 22 | if (minX > maxX) { return false; } 23 | 24 | float minY = Mathf.Min(p1.y, p2.y); 25 | float maxY = Mathf.Max(p1.y, p2.y); 26 | 27 | float dx = p2.x - p1.x; 28 | 29 | if (Mathf.Abs(dx) > float.Epsilon) 30 | { 31 | float a = (p2.y - p1.y) / dx; 32 | float b = p1.y - a * p1.x; 33 | minY = a * minX + b; 34 | maxY = a * maxX + b; 35 | } 36 | 37 | if (minY > maxY) 38 | { 39 | float tmp = maxY; 40 | maxY = minY; 41 | minY = tmp; 42 | } 43 | 44 | if (maxY > rect.yMax) { maxY = rect.yMax; } 45 | 46 | if (minY < rect.yMin) { minY = rect.yMin; } 47 | 48 | if (minY > maxY) { return false; } 49 | 50 | return true; 51 | } 52 | 53 | public static Rect Encompass(Rect a, Rect b) => 54 | new() 55 | { 56 | xMin = Math.Min(a.xMin, b.xMin), 57 | yMin = Math.Min(a.yMin, b.yMin), 58 | xMax = Math.Max(a.xMax, b.xMax), 59 | yMax = Math.Max(a.yMax, b.yMax) 60 | }; 61 | 62 | public static Rect Inflate(Rect a, float left, float top, float right, float bottom) => 63 | new() 64 | { 65 | xMin = a.xMin - left, 66 | yMin = a.yMin - top, 67 | xMax = a.xMax + right, 68 | yMax = a.yMax + bottom 69 | }; 70 | 71 | internal static Rect Rect(this VisualElement ve) => new(Vector2.zero, ve.layout.size); 72 | } 73 | } --------------------------------------------------------------------------------