mirror of
https://github.com/4sval/FModel.git
synced 2026-06-22 16:00:17 -05:00
Add state machine overview layers with Entry, State nodes, and Transition connections
Parse States and Transitions from BakedStateMachines to build state-level overview layers for each state machine. Each overview shows: - Entry node (filled circle) connecting to the initial state - State nodes (rounded rectangles) with their names - Directional transition arrows between states The overview layer replaces the old internal per-state layer and is opened via double-click on StateMachine nodes in the parent graph. Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com>
This commit is contained in:
parent
58a0a50ac5
commit
22ba7668eb
|
|
@ -15,6 +15,8 @@ public class AnimGraphNode
|
|||
public string NodeComment { get; set; } = string.Empty;
|
||||
public int NodePosX { get; set; }
|
||||
public int NodePosY { get; set; }
|
||||
public bool IsStateMachineState { get; set; }
|
||||
public bool IsEntryNode { get; set; }
|
||||
public List<AnimGraphPin> Pins { get; set; } = [];
|
||||
public Dictionary<string, string> AdditionalProperties { get; set; } = new();
|
||||
|
||||
|
|
@ -50,11 +52,24 @@ public class AnimGraphLayer
|
|||
public List<AnimGraphConnection> Connections { get; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds metadata extracted from BakedStateMachines for building
|
||||
/// state machine overview layers (Entry + State nodes + Transition connections).
|
||||
/// </summary>
|
||||
internal class StateMachineMetadata
|
||||
{
|
||||
public string MachineName { get; init; } = string.Empty;
|
||||
public List<string> StateNames { get; } = [];
|
||||
public List<(int PreviousState, int NextState)> Transitions { get; } = [];
|
||||
}
|
||||
|
||||
public class AnimGraphViewModel
|
||||
{
|
||||
private const int GridColumns = 4;
|
||||
private const int NodeHorizontalSpacing = 300;
|
||||
private const int NodeVerticalSpacing = 200;
|
||||
private const int StateNodeHorizontalSpacing = 250;
|
||||
private const int StateNodeVerticalSpacing = 150;
|
||||
private const int MaxPropertyValueDisplayLength = 100;
|
||||
internal const string SubGraphPathSeparator = " > ";
|
||||
|
||||
|
|
@ -133,7 +148,9 @@ public class AnimGraphViewModel
|
|||
}
|
||||
|
||||
// Associate state machine nodes with their baked machine names
|
||||
AssociateStateMachineNames(animBlueprintClass, cdo, animNodeProps, nodeByName);
|
||||
// and collect state machine metadata for overview layers
|
||||
var smMetadata = new List<StateMachineMetadata>();
|
||||
AssociateStateMachineNames(animBlueprintClass, cdo, animNodeProps, nodeByName, smMetadata);
|
||||
|
||||
// Group nodes into layers (connected subgraphs)
|
||||
BuildLayers(vm);
|
||||
|
|
@ -141,6 +158,9 @@ public class AnimGraphViewModel
|
|||
// Prefix state machine internal layers with their parent path to avoid name collisions
|
||||
PrefixStateMachineLayerNames(vm);
|
||||
|
||||
// Build state machine overview layers (Entry + State nodes + Transitions)
|
||||
BuildStateMachineOverviewLayers(vm, smMetadata);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
|
|
@ -257,6 +277,142 @@ public class AnimGraphViewModel
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates state machine overview layers with synthetic Entry + State nodes
|
||||
/// and transition connections between states, providing a UE-like state machine
|
||||
/// editor view. The overview layer is named with the path prefix to match
|
||||
/// double-click navigation from StateMachine nodes.
|
||||
/// </summary>
|
||||
private static void BuildStateMachineOverviewLayers(AnimGraphViewModel vm, List<StateMachineMetadata> smMetadata)
|
||||
{
|
||||
// Map: machineName → parent layer name (where the StateMachine node lives)
|
||||
var smParentLayer = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var layer in vm.Layers)
|
||||
{
|
||||
foreach (var node in layer.Nodes)
|
||||
{
|
||||
if (node.AdditionalProperties.TryGetValue("StateMachineName", out var machineName))
|
||||
smParentLayer.TryAdd(machineName, layer.Name);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sm in smMetadata)
|
||||
{
|
||||
if (sm.StateNames.Count == 0) continue;
|
||||
|
||||
// Determine the path-prefixed layer name
|
||||
var parentName = smParentLayer.GetValueOrDefault(sm.MachineName, "AnimGraph");
|
||||
var overviewLayerName = $"{parentName}{SubGraphPathSeparator}{sm.MachineName}";
|
||||
|
||||
// Remove existing internal layers with this name (they'll be replaced by the overview)
|
||||
vm.Layers.RemoveAll(l => l.Name.Equals(overviewLayerName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var overviewLayer = new AnimGraphLayer { Name = overviewLayerName };
|
||||
var stateNodes = new List<AnimGraphNode>();
|
||||
|
||||
// Create Entry node
|
||||
var entryNode = new AnimGraphNode
|
||||
{
|
||||
Name = "Entry",
|
||||
ExportType = "Entry",
|
||||
IsEntryNode = true
|
||||
};
|
||||
entryNode.Pins.Add(new AnimGraphPin
|
||||
{
|
||||
PinName = "Output",
|
||||
IsOutput = true,
|
||||
PinType = "transition",
|
||||
OwnerNode = entryNode
|
||||
});
|
||||
overviewLayer.Nodes.Add(entryNode);
|
||||
|
||||
// Create State nodes
|
||||
for (var i = 0; i < sm.StateNames.Count; i++)
|
||||
{
|
||||
var stateNode = new AnimGraphNode
|
||||
{
|
||||
Name = sm.StateNames[i],
|
||||
ExportType = "State",
|
||||
IsStateMachineState = true
|
||||
};
|
||||
stateNode.Pins.Add(new AnimGraphPin
|
||||
{
|
||||
PinName = "In",
|
||||
IsOutput = false,
|
||||
PinType = "transition",
|
||||
OwnerNode = stateNode
|
||||
});
|
||||
stateNode.Pins.Add(new AnimGraphPin
|
||||
{
|
||||
PinName = "Out",
|
||||
IsOutput = true,
|
||||
PinType = "transition",
|
||||
OwnerNode = stateNode
|
||||
});
|
||||
stateNodes.Add(stateNode);
|
||||
overviewLayer.Nodes.Add(stateNode);
|
||||
}
|
||||
|
||||
// Entry connects to first state (state index 0)
|
||||
if (stateNodes.Count > 0)
|
||||
{
|
||||
overviewLayer.Connections.Add(new AnimGraphConnection
|
||||
{
|
||||
SourceNode = entryNode,
|
||||
SourcePinName = "Output",
|
||||
TargetNode = stateNodes[0],
|
||||
TargetPinName = "In"
|
||||
});
|
||||
}
|
||||
|
||||
// Transition connections between states
|
||||
foreach (var (prevIdx, nextIdx) in sm.Transitions)
|
||||
{
|
||||
if (prevIdx < stateNodes.Count && nextIdx < stateNodes.Count)
|
||||
{
|
||||
overviewLayer.Connections.Add(new AnimGraphConnection
|
||||
{
|
||||
SourceNode = stateNodes[prevIdx],
|
||||
SourcePinName = "Out",
|
||||
TargetNode = stateNodes[nextIdx],
|
||||
TargetPinName = "In"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Layout state nodes in a grid arrangement
|
||||
LayoutStateMachineOverview(overviewLayer, entryNode, stateNodes);
|
||||
|
||||
vm.Layers.Add(overviewLayer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Arranges state machine overview nodes: Entry on the left, state nodes in a grid.
|
||||
/// </summary>
|
||||
private static void LayoutStateMachineOverview(AnimGraphLayer layer, AnimGraphNode entryNode, List<AnimGraphNode> stateNodes)
|
||||
{
|
||||
// Place Entry on the far left
|
||||
entryNode.NodePosX = 0;
|
||||
entryNode.NodePosY = 0;
|
||||
|
||||
if (stateNodes.Count == 0) return;
|
||||
|
||||
// Arrange state nodes in a grid to the right of Entry
|
||||
var cols = Math.Max(1, (int)Math.Ceiling(Math.Sqrt(stateNodes.Count)));
|
||||
for (var i = 0; i < stateNodes.Count; i++)
|
||||
{
|
||||
var col = i % cols;
|
||||
var row = i / cols;
|
||||
stateNodes[i].NodePosX = StateNodeHorizontalSpacing + col * StateNodeHorizontalSpacing;
|
||||
stateNodes[i].NodePosY = row * StateNodeVerticalSpacing;
|
||||
}
|
||||
|
||||
// Center Entry vertically relative to state nodes
|
||||
var maxRow = (stateNodes.Count - 1) / cols;
|
||||
entryNode.NodePosY = maxRow * StateNodeVerticalSpacing / 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines a display name for a layer based on the types of nodes it contains.
|
||||
/// A Root node's "Name" property defines the layer/sub-graph name
|
||||
|
|
@ -307,12 +463,13 @@ public class AnimGraphViewModel
|
|||
|
||||
/// <summary>
|
||||
/// Reads BakedStateMachines from the animation blueprint class to associate
|
||||
/// FAnimNode_StateMachine nodes with their machine names and mark internal
|
||||
/// state root nodes so they can be grouped into correctly named layers.
|
||||
/// FAnimNode_StateMachine nodes with their machine names, mark internal
|
||||
/// state root nodes, and collect state/transition metadata for overview layers.
|
||||
/// </summary>
|
||||
private static void AssociateStateMachineNames(UClass animBlueprintClass, UObject? cdo,
|
||||
List<(string name, string structType)> animNodeProps,
|
||||
Dictionary<string, AnimGraphNode> nodeByName)
|
||||
Dictionary<string, AnimGraphNode> nodeByName,
|
||||
List<StateMachineMetadata> smMetadata)
|
||||
{
|
||||
// BakedStateMachines is a UPROPERTY on UAnimBlueprintGeneratedClass
|
||||
// Try reading from both the class and CDO
|
||||
|
|
@ -357,17 +514,30 @@ public class AnimGraphViewModel
|
|||
smNode.AdditionalProperties["StateMachineName"] = machineName;
|
||||
}
|
||||
|
||||
// Mark state root nodes with BelongsToStateMachine so their layers get the machine name
|
||||
var metadata = new StateMachineMetadata { MachineName = machineName };
|
||||
|
||||
// Extract state names and mark root nodes with BelongsToStateMachine
|
||||
foreach (var prop in machineStruct.Properties)
|
||||
{
|
||||
if (prop.Name.Text != "States") continue;
|
||||
if (prop.Tag?.GenericValue is not UScriptArray states) break;
|
||||
|
||||
foreach (var stateProp in states.Properties)
|
||||
for (var stateIdx = 0; stateIdx < states.Properties.Count; stateIdx++)
|
||||
{
|
||||
if (stateProp.GetValue(typeof(FStructFallback)) is not FStructFallback stateStruct)
|
||||
if (states.Properties[stateIdx].GetValue(typeof(FStructFallback)) is not FStructFallback stateStruct)
|
||||
{
|
||||
metadata.StateNames.Add($"State_{stateIdx}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract state name
|
||||
var stateName = $"State_{stateIdx}";
|
||||
if (stateStruct.TryGetValue(out FName stateNameProp, "StateName"))
|
||||
stateName = stateNameProp.Text;
|
||||
|
||||
metadata.StateNames.Add(stateName);
|
||||
|
||||
// Mark root node
|
||||
if (!stateStruct.TryGetValue(out int stateRootIndex, "StateRootNodeIndex"))
|
||||
continue;
|
||||
|
||||
|
|
@ -380,6 +550,33 @@ public class AnimGraphViewModel
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract machine-level transitions (PreviousState → NextState)
|
||||
foreach (var prop in machineStruct.Properties)
|
||||
{
|
||||
if (prop.Name.Text != "Transitions") continue;
|
||||
if (prop.Tag?.GenericValue is not UScriptArray transitions) break;
|
||||
|
||||
foreach (var transProp in transitions.Properties)
|
||||
{
|
||||
if (transProp.GetValue(typeof(FStructFallback)) is not FStructFallback transStruct)
|
||||
continue;
|
||||
|
||||
if (!transStruct.TryGetValue(out int previousState, "PreviousState"))
|
||||
continue;
|
||||
if (!transStruct.TryGetValue(out int nextState, "NextState"))
|
||||
continue;
|
||||
|
||||
if (previousState >= 0 && nextState >= 0 &&
|
||||
previousState < metadata.StateNames.Count && nextState < metadata.StateNames.Count)
|
||||
{
|
||||
metadata.Transitions.Add((previousState, nextState));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
smMetadata.Add(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ public partial class AnimGraphViewer
|
|||
private const double PinLabelOffset = 14; // PinCircleRadius * 2 + padding
|
||||
private const double HeaderGradientDarkenFactor = 0.6;
|
||||
private const double DefaultGraphWidthRatio = 0.65;
|
||||
private const double StateNodeWidth = 180;
|
||||
private const double StateNodeHeight = 50;
|
||||
private const double StateNodeCornerRadius = 24;
|
||||
private const double EntryNodeSize = 30;
|
||||
private const double TransitionArrowSize = 10;
|
||||
|
||||
private readonly AnimGraphViewModel _viewModel;
|
||||
|
||||
|
|
@ -160,17 +165,22 @@ public partial class AnimGraphViewer
|
|||
state.NodePositions[node] = new Point(node.NodePosX, node.NodePosY);
|
||||
}
|
||||
|
||||
// Draw nodes
|
||||
foreach (var node in state.Layer.Nodes)
|
||||
{
|
||||
DrawNode(state, node);
|
||||
}
|
||||
|
||||
// Draw connections
|
||||
// Draw connections first (behind nodes) for state machine overview
|
||||
foreach (var conn in state.Layer.Connections)
|
||||
{
|
||||
DrawConnectionLine(state, conn);
|
||||
}
|
||||
|
||||
// Draw nodes
|
||||
foreach (var node in state.Layer.Nodes)
|
||||
{
|
||||
if (node.IsEntryNode)
|
||||
DrawEntryNode(state, node);
|
||||
else if (node.IsStateMachineState)
|
||||
DrawStateNode(state, node);
|
||||
else
|
||||
DrawNode(state, node);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNode(LayerCanvasState state, AnimGraphNode node)
|
||||
|
|
@ -321,6 +331,142 @@ public partial class AnimGraphViewer
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws an Entry node as a small filled circle, matching UE's state machine editor.
|
||||
/// </summary>
|
||||
private void DrawEntryNode(LayerCanvasState state, AnimGraphNode node)
|
||||
{
|
||||
var pos = state.NodePositions[node];
|
||||
|
||||
var circle = new Ellipse
|
||||
{
|
||||
Width = EntryNodeSize,
|
||||
Height = EntryNodeSize,
|
||||
Fill = new SolidColorBrush(Color.FromRgb(80, 80, 80)),
|
||||
Stroke = new SolidColorBrush(Color.FromRgb(200, 200, 200)),
|
||||
StrokeThickness = 2,
|
||||
SnapsToDevicePixels = true
|
||||
};
|
||||
Canvas.SetLeft(circle, pos.X);
|
||||
Canvas.SetTop(circle, pos.Y);
|
||||
Panel.SetZIndex(circle, 1);
|
||||
state.Canvas.Children.Add(circle);
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = "Entry",
|
||||
Foreground = Brushes.White,
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
TextAlignment = TextAlignment.Center
|
||||
};
|
||||
label.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
Canvas.SetLeft(label, pos.X + EntryNodeSize / 2 - label.DesiredSize.Width / 2);
|
||||
Canvas.SetTop(label, pos.Y + EntryNodeSize + 4);
|
||||
Panel.SetZIndex(label, 1);
|
||||
state.Canvas.Children.Add(label);
|
||||
|
||||
// Output pin position (right edge of circle)
|
||||
state.PinPositions[(node, "Output", true)] = new Point(
|
||||
pos.X + EntryNodeSize, pos.Y + EntryNodeSize / 2);
|
||||
|
||||
// Store visuals with a dummy border for selection
|
||||
var hitArea = new Border
|
||||
{
|
||||
Width = EntryNodeSize,
|
||||
Height = EntryNodeSize,
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(EntryNodeSize / 2)
|
||||
};
|
||||
Canvas.SetLeft(hitArea, pos.X);
|
||||
Canvas.SetTop(hitArea, pos.Y);
|
||||
Panel.SetZIndex(hitArea, 2);
|
||||
state.Canvas.Children.Add(hitArea);
|
||||
state.NodeVisuals[node] = (hitArea, EntryNodeSize, EntryNodeSize);
|
||||
|
||||
hitArea.MouseLeftButtonDown += (s, e) =>
|
||||
{
|
||||
SelectNode(node, hitArea);
|
||||
e.Handled = true;
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a state machine state node as a rounded rectangle with a centered name,
|
||||
/// matching UE's state machine editor visual style.
|
||||
/// </summary>
|
||||
private void DrawStateNode(LayerCanvasState state, AnimGraphNode node)
|
||||
{
|
||||
var pos = state.NodePositions[node];
|
||||
|
||||
// Shadow
|
||||
var shadow = new Border
|
||||
{
|
||||
Width = StateNodeWidth,
|
||||
Height = StateNodeHeight,
|
||||
CornerRadius = new CornerRadius(StateNodeCornerRadius),
|
||||
Background = Brushes.Black,
|
||||
Opacity = 0.4,
|
||||
Effect = new BlurEffect { Radius = 6 }
|
||||
};
|
||||
Canvas.SetLeft(shadow, pos.X + 2);
|
||||
Canvas.SetTop(shadow, pos.Y + 2);
|
||||
Panel.SetZIndex(shadow, 0);
|
||||
state.Canvas.Children.Add(shadow);
|
||||
|
||||
// State body
|
||||
var border = new Border
|
||||
{
|
||||
Width = StateNodeWidth,
|
||||
Height = StateNodeHeight,
|
||||
CornerRadius = new CornerRadius(StateNodeCornerRadius),
|
||||
Background = new SolidColorBrush(Color.FromArgb(240, 55, 55, 55)),
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(120, 120, 120)),
|
||||
BorderThickness = new Thickness(2),
|
||||
SnapsToDevicePixels = true
|
||||
};
|
||||
|
||||
var nameText = new TextBlock
|
||||
{
|
||||
Text = node.Name,
|
||||
Foreground = Brushes.White,
|
||||
FontSize = 13,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
TextAlignment = TextAlignment.Center
|
||||
};
|
||||
border.Child = nameText;
|
||||
|
||||
Canvas.SetLeft(border, pos.X);
|
||||
Canvas.SetTop(border, pos.Y);
|
||||
Panel.SetZIndex(border, 1);
|
||||
state.Canvas.Children.Add(border);
|
||||
|
||||
state.NodeVisuals[node] = (border, StateNodeWidth, StateNodeHeight);
|
||||
|
||||
// Pin positions (left = input, right = output)
|
||||
state.PinPositions[(node, "In", false)] = new Point(
|
||||
pos.X, pos.Y + StateNodeHeight / 2);
|
||||
state.PinPositions[(node, "Out", true)] = new Point(
|
||||
pos.X + StateNodeWidth, pos.Y + StateNodeHeight / 2);
|
||||
|
||||
border.ToolTip = $"State: {node.Name}";
|
||||
|
||||
border.MouseLeftButtonDown += (s, e) =>
|
||||
{
|
||||
if (e.ClickCount == 2)
|
||||
{
|
||||
TryOpenSubGraph(node);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
SelectNode(node, border);
|
||||
e.Handled = true;
|
||||
};
|
||||
}
|
||||
|
||||
private void AddPinVisual(Canvas pinsCanvas, AnimGraphPin pin, double y, bool isInput, Color pinColor)
|
||||
{
|
||||
var displayName = string.IsNullOrEmpty(pin.PinName) ? "(unnamed)" : pin.PinName;
|
||||
|
|
@ -415,6 +561,13 @@ public partial class AnimGraphViewer
|
|||
{
|
||||
node.AdditionalProperties.TryGetValue("Layer", out layerName);
|
||||
}
|
||||
else if (node.IsStateMachineState)
|
||||
{
|
||||
// State nodes within an overview: try to open internal per-state layer
|
||||
// Internal layers share the state machine's path prefix name
|
||||
// (future: individual state layers could be opened here)
|
||||
return;
|
||||
}
|
||||
else if (node.ExportType.Contains("StateMachine", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// State machine internal layers are prefixed with parent path
|
||||
|
|
@ -571,28 +724,87 @@ public partial class AnimGraphViewer
|
|||
// Determine wire color from source pin type
|
||||
var sourcePin = conn.SourceNode.Pins.FirstOrDefault(p => p.PinName == conn.SourcePinName && p.IsOutput);
|
||||
var wireColor = sourcePin != null ? GetPinColor(sourcePin.PinType) : Color.FromRgb(200, 200, 220);
|
||||
var isTransition = (conn.SourceNode.IsStateMachineState || conn.SourceNode.IsEntryNode) &&
|
||||
(conn.TargetNode.IsStateMachineState || conn.TargetNode.IsEntryNode);
|
||||
|
||||
var dx = Math.Max(Math.Abs(endPos.X - startPos.X) * 0.5, 50);
|
||||
var pathFigure = new PathFigure { StartPoint = startPos };
|
||||
pathFigure.Segments.Add(new BezierSegment(
|
||||
new Point(startPos.X + dx, startPos.Y),
|
||||
new Point(endPos.X - dx, endPos.Y),
|
||||
endPos, true));
|
||||
|
||||
var pathGeometry = new PathGeometry();
|
||||
pathGeometry.Figures.Add(pathFigure);
|
||||
|
||||
var path = new Path
|
||||
if (isTransition)
|
||||
{
|
||||
Data = pathGeometry,
|
||||
Stroke = new SolidColorBrush(wireColor),
|
||||
DrawTransitionArrow(state, startPos, endPos, wireColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dx = Math.Max(Math.Abs(endPos.X - startPos.X) * 0.5, 50);
|
||||
var pathFigure = new PathFigure { StartPoint = startPos };
|
||||
pathFigure.Segments.Add(new BezierSegment(
|
||||
new Point(startPos.X + dx, startPos.Y),
|
||||
new Point(endPos.X - dx, endPos.Y),
|
||||
endPos, true));
|
||||
|
||||
var pathGeometry = new PathGeometry();
|
||||
pathGeometry.Figures.Add(pathFigure);
|
||||
|
||||
var path = new Path
|
||||
{
|
||||
Data = pathGeometry,
|
||||
Stroke = new SolidColorBrush(wireColor),
|
||||
StrokeThickness = 2.5,
|
||||
Opacity = 0.85,
|
||||
SnapsToDevicePixels = true,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
Panel.SetZIndex(path, 0);
|
||||
state.Canvas.Children.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a directional transition arrow between state machine state nodes,
|
||||
/// with an arrowhead at the target end.
|
||||
/// </summary>
|
||||
private static void DrawTransitionArrow(LayerCanvasState state, Point startPos, Point endPos, Color wireColor)
|
||||
{
|
||||
var brush = new SolidColorBrush(wireColor);
|
||||
|
||||
// Main line
|
||||
var line = new Line
|
||||
{
|
||||
X1 = startPos.X,
|
||||
Y1 = startPos.Y,
|
||||
X2 = endPos.X,
|
||||
Y2 = endPos.Y,
|
||||
Stroke = brush,
|
||||
StrokeThickness = 2.5,
|
||||
Opacity = 0.85,
|
||||
SnapsToDevicePixels = true,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
Panel.SetZIndex(path, 0);
|
||||
state.Canvas.Children.Add(path);
|
||||
Panel.SetZIndex(line, 0);
|
||||
state.Canvas.Children.Add(line);
|
||||
|
||||
// Arrowhead
|
||||
var dx = endPos.X - startPos.X;
|
||||
var dy = endPos.Y - startPos.Y;
|
||||
var length = Math.Sqrt(dx * dx + dy * dy);
|
||||
if (length < 1) return;
|
||||
|
||||
var ux = dx / length;
|
||||
var uy = dy / length;
|
||||
var arrowBase = new Point(endPos.X - ux * TransitionArrowSize, endPos.Y - uy * TransitionArrowSize);
|
||||
var perpX = -uy * TransitionArrowSize * 0.5;
|
||||
var perpY = ux * TransitionArrowSize * 0.5;
|
||||
|
||||
var arrowHead = new Polygon
|
||||
{
|
||||
Points =
|
||||
[
|
||||
endPos,
|
||||
new Point(arrowBase.X + perpX, arrowBase.Y + perpY),
|
||||
new Point(arrowBase.X - perpX, arrowBase.Y - perpY)
|
||||
],
|
||||
Fill = brush,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
Panel.SetZIndex(arrowHead, 0);
|
||||
state.Canvas.Children.Add(arrowHead);
|
||||
}
|
||||
|
||||
private static string GetNodeDisplayName(AnimGraphNode node)
|
||||
|
|
@ -642,6 +854,7 @@ public partial class AnimGraphViewer
|
|||
"string" or "text" or "name" => Color.FromRgb(255, 80, 180),
|
||||
"delegate" => Color.FromRgb(255, 56, 56),
|
||||
"pose" => Color.FromRgb(0, 160, 100),
|
||||
"transition" => Color.FromRgb(200, 200, 200),
|
||||
_ => Color.FromRgb(180, 180, 200)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user