/// <summary> /// Takes a list of nodes and generates unique names for them. Returns a list of node + name pairs. /// The names are chosen to be descriptive and usable in code generation. /// </summary> /// <returns>A lot of node + name pairs usable in code generation.</returns> public static IEnumerable <(TNode, string)> GenerateNodeNames(IEnumerable <TNode> nodes) { var nodesByName = new Dictionary <NodeName, List <TNode> >(); foreach (var node in nodes) { // Generate descriptive name for each node. The name is generated based on its type // and properties to give as much information about the node as possible, so that // a specific node can be identified in the composition. var nodeName = node.Type switch { Graph.NodeType.CompositionObject => NameCompositionObject(node, (CompositionObject)node.Object), Graph.NodeType.CompositionPath => NodeName.FromNonTypeName("Path"), Graph.NodeType.CanvasGeometry => NodeName.FromNonTypeName("Geometry"), Graph.NodeType.LoadedImageSurface => NameLoadedImageSurface(node, (LoadedImageSurface)node.Object), _ => throw Unreachable, }; if (!nodesByName.TryGetValue(nodeName, out var nodeList)) { nodeList = new List <TNode>(); nodesByName.Add(nodeName, nodeList); } nodeList.Add(node); } // Set the names on each node. foreach (var entry in nodesByName) { var nodeName = entry.Key; var nodeList = entry.Value; // Append a counter suffix. // NOTE: For C# there is no need for a suffix if there is only one node with this name, // however this can break C++ which cannot distinguish between a method name and // a type name. For example, if a single CompositionPath node produced a method // called CompositionPath() and then a call was made to "new CompositionPath(...)" // the C++ compiler will complain that CompositionPath is not a type. // So to ensure we don't hit that case, append a counter suffix, unless the name // is known to not be a type name. if (nodeList.Count == 1 && nodeName.IsNotATypeName) { // The name is unique and is not a type name, so no need for a suffix. yield return(nodeList[0], nodeName.Name); } else { // Use only as many digits as necessary to express the largest count. var digitsRequired = (int)Math.Ceiling(Math.Log10(nodeList.Count)); var counterFormat = new string('0', digitsRequired); for (var i = 0; i < nodeList.Count; i++) { yield return(nodeList[i], $"{nodeName.Name}_{i.ToString(counterFormat)}"); } } } }
static NodeName NameCompositionObject(TNode node, CompositionObject obj) { var name = NameOf(obj); if (name != null) { // The object has a name, so use it. return(NodeName.FromNonTypeName(name)); } return(obj.Type switch { // For some animations, we can include a description of the start and end values // to make the names more descriptive. CompositionObjectType.ColorKeyFrameAnimation => NodeName.FromNameAndDescription("ColorAnimation", DescribeAnimationRange((ColorKeyFrameAnimation)obj)), CompositionObjectType.ScalarKeyFrameAnimation => NodeName.FromNameAndDescription($"{TryGetAnimatedPropertyName(node)}ScalarAnimation", DescribeAnimationRange((ScalarKeyFrameAnimation)obj)), // Do not include descriptions of the animation range for vectors - the names // end up being very long, complicated, and confusing to the reader. CompositionObjectType.Vector2KeyFrameAnimation => NodeName.FromNonTypeName($"{TryGetAnimatedPropertyName(node)}Vector2Animation"), CompositionObjectType.Vector3KeyFrameAnimation => NodeName.FromNonTypeName($"{TryGetAnimatedPropertyName(node)}Vector3Animation"), CompositionObjectType.Vector4KeyFrameAnimation => NodeName.FromNonTypeName($"{TryGetAnimatedPropertyName(node)}Vector4Animation"), // Boolean animations don't have interesting range descriptions, but their property name // is helpful to know (it is typically "IsVisible"). CompositionObjectType.BooleanKeyFrameAnimation => NodeName.FromNonTypeName($"{TryGetAnimatedPropertyName(node)}BooleanAnimation"), // Geometries include their size as part of the description. CompositionObjectType.CompositionRectangleGeometry => NodeName.FromNameAndDescription("Rectangle", Vector2AsId(((CompositionRectangleGeometry)obj).Size)), CompositionObjectType.CompositionRoundedRectangleGeometry => NodeName.FromNameAndDescription("RoundedRectangle", Vector2AsId(((CompositionRoundedRectangleGeometry)obj).Size)), CompositionObjectType.CompositionEllipseGeometry => NodeName.FromNameAndDescription("Ellipse", Vector2AsId(((CompositionEllipseGeometry)obj).Radius)), CompositionObjectType.ExpressionAnimation => NameExpressionAnimation((ExpressionAnimation)obj), CompositionObjectType.CompositionColorBrush => NameCompositionColorBrush((CompositionColorBrush)obj), CompositionObjectType.CompositionColorGradientStop => NameCompositionColorGradientStop((CompositionColorGradientStop)obj), CompositionObjectType.StepEasingFunction => NameStepEasingFunction((StepEasingFunction)obj), _ => NameCompositionObjectType(obj.Type), });
public static IEnumerable <(TNode, string)> GenerateNodeNames(IEnumerable <TNode> nodes) { var nodesByName = new Dictionary <NodeName, List <TNode> >(); foreach (var node in nodes) { // Generate descriptive name for each node. The name is generated based on its type // and properties to give as much information about the node as possible, so that // a specific node can be identified in the composition. var nodeName = node.Type switch { Graph.NodeType.CompositionObject => NameCompositionObject(node, (CompositionObject)node.Object), Graph.NodeType.CompositionPath => NodeName.FromNonTypeName("Path"), Graph.NodeType.CanvasGeometry => NodeName.FromNonTypeName("Geometry"), Graph.NodeType.LoadedImageSurface => NameLoadedImageSurface(node, (LoadedImageSurface)node.Object), _ => throw Unreachable, }; if (!nodesByName.TryGetValue(nodeName, out var nodeList)) { nodeList = new List <TNode>(); nodesByName.Add(nodeName, nodeList); } nodeList.Add(node); } // Set the names on each node. var uniqueNames = new HashSet <string>(); // First deal with names that we know are unique. foreach (var(nodeName, nodeList) in nodesByName) { // NOTE: For C# there is no need for a suffix if there is only one node with this name, // however this can break C++ which cannot distinguish between a method name and // a type name. For example, if a single CompositionPath node produced a method // called CompositionPath() and then a call was made to "new CompositionPath(...)" // the C++ compiler will complain that CompositionPath is not a type. // So to ensure we don't hit that case, append a counter suffix, unless the name // is known to not be a type name. if (nodeList.Count == 1 && nodeName.IsNotATypeName) { // The name is unique and is not a type name, so no need for a suffix. var name = nodeName.Name; uniqueNames.Add(name); yield return(nodeList[0], name); } } // Now deal with the names that are not unique by appending a counter suffix. foreach (var(nodeName, nodeList) in nodesByName) { if (nodeList.Count > 1 || !nodeName.IsNotATypeName) { // Use only as many digits as necessary to express the largest count. var digitsRequired = (int)Math.Ceiling(Math.Log10(nodeList.Count)); var counterFormat = new string('0', digitsRequired); var suffixOffset = 0; for (var i = 0; i < nodeList.Count; i++) { // Create a unique name by appending a suffix. // If the name already exists then increment the suffix until a unique // name is found. This is necessary to deal with collisions with the // names that were known to be unique but that have names that look // like they have counter suffixes, for example Rectangle_15 could // be a 15x15 rectangle, or it could be the 15th rectangle with an // animated size. string name; while (true) { var counter = i + suffixOffset; name = $"{nodeName.Name}_{counter.ToString(counterFormat)}"; if (uniqueNames.Add(name)) { // The name was unique. break; } // Try the next suffix value. suffixOffset++; } yield return(nodeList[i], name); } } } }